Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
403ce93990 | ||
|
|
252e5d1cff | ||
|
|
5af4d6c368 | ||
|
|
ddde177776 | ||
|
|
e69d139487 | ||
|
|
3f0d15c9e3 | ||
|
|
bbd2d83364 | ||
|
|
c5588ebba1 | ||
|
|
f6838e4696 | ||
|
|
36db4223f2 | ||
|
|
80748e6c94 | ||
|
|
c5086510f0 | ||
|
|
5b1a868ccf | ||
|
|
c36bd60022 | ||
|
|
baa053d250 | ||
|
|
ce41a8cf34 | ||
|
|
783bca806a | ||
|
|
088c8a5986 | ||
|
|
b951eae58c | ||
|
|
ea847fea44 | ||
|
|
06ae9ca8d3 | ||
|
|
be85cb71c9 | ||
|
|
ed3cb4aaac | ||
|
|
f37424d9fc | ||
|
|
96db0b7a86 | ||
|
|
e15f8896c5 | ||
|
|
fc53ad61ee | ||
|
|
4ab63d08ae | ||
|
|
ef19c1448e | ||
|
|
b2fbe91de9 | ||
|
|
32c6ae98a1 | ||
|
|
fa696efa8b | ||
|
|
610023ef6a | ||
|
|
2f1ec65b30 | ||
|
|
acdfaaecd7 | ||
|
|
5c654723c2 | ||
|
|
5b979a0540 | ||
|
|
f59f4778c9 | ||
|
|
2b4a158423 | ||
|
|
ef8a82e4c3 | ||
|
|
783dabbea5 | ||
|
|
b6feac2200 | ||
|
|
bd746c61d7 | ||
|
|
3b86f6c3b6 | ||
|
|
8d1af89e1a | ||
|
|
53c83f690c | ||
|
|
5be1504cfc | ||
|
|
f861d78114 | ||
|
|
92678c4157 | ||
|
|
2fa0bef6c2 | ||
|
|
049d8c781c | ||
|
|
57c6fddfc3 | ||
|
|
1e92c5b2a2 | ||
|
|
6bfaa518d7 | ||
|
|
7ef7f75246 | ||
|
|
967d3d95de | ||
|
|
7b8685d038 | ||
|
|
9d8b19d14a | ||
|
|
20d88e1d9c | ||
|
|
60b743e6c2 | ||
|
|
dca7d99c1b | ||
|
|
656c4d4d7f | ||
|
|
0730b2090e | ||
|
|
60e08e802e | ||
|
|
f62fe23826 | ||
|
|
75207f0d1e | ||
|
|
83c488cc48 | ||
|
|
5e5d157496 | ||
|
|
72b09d60a6 | ||
|
|
af9f89b628 | ||
|
|
981b95718e | ||
|
|
8ce050ef7f |
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"version": "0.0.1",
|
||||
"configurations": [
|
||||
{
|
||||
"name": "mockups",
|
||||
"runtimeExecutable": "python",
|
||||
"runtimeArgs": ["-m", "http.server", "8765", "--bind", "127.0.0.1"],
|
||||
"port": 8765
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
---
|
||||
description: The panel's design is fixed by accepted mockups; implement it, do not reinterpret it.
|
||||
globs: internal/web/view/**,docs/assets/panel-redesign/**
|
||||
alwaysApply: false
|
||||
---
|
||||
|
||||
# Panel design contract
|
||||
|
||||
Source of truth, in this order: the mockups in
|
||||
[docs/assets/panel-redesign/panel/](docs/assets/panel-redesign/index.html),
|
||||
the [components page](docs/assets/panel-redesign/panel/components.html),
|
||||
`panel/outlines.json`, then
|
||||
[docs/plans/panel-redesign.md](docs/plans/panel-redesign.md). Read the plan's
|
||||
**The contract** and **Enforcement** sections before touching a template.
|
||||
|
||||
You are implementing a finished design. You are not designing.
|
||||
|
||||
- Build pages only from Bulma classes in the `BULMA` list of `panel/check.py`
|
||||
and from the `sp-` components on the components page, through their
|
||||
partials. No other class, no hand-written component markup, no inline style.
|
||||
- No selector in `panel.css` that names a page or an id, uses `!important`, or
|
||||
sets a width outside `WIDTH_OK`. If a page looks wrong, the layout choice is
|
||||
wrong — do not patch it locally.
|
||||
- If a screen seems to need a component, state or layout that does not exist:
|
||||
**stop and ask.** Do not approximate it, and do not substitute a look of
|
||||
your own.
|
||||
- Never edit a mockup, `outlines.json`, `check.py`, the vendored Bulma or a
|
||||
guard test to make your work pass. A failing guard means the implementation
|
||||
is wrong. Design changes go mockup-first in a separate `design:` commit on
|
||||
the owner's explicit instruction.
|
||||
- "Done" is evidence, not a description: passing `go test ./internal/web/...`
|
||||
output plus a screenshot of each restyled page from the running container,
|
||||
saved under `docs/assets/panel-redesign/evidence/` beside its mockup's name.
|
||||
Report every difference from the mockup you could not remove; do not
|
||||
describe a different result as the requested one.
|
||||
|
||||
After editing a fragment in `panel/src/`: `python panel/build.py`, then
|
||||
`python panel/check.py` (add `--write` only when the structure change is the
|
||||
point of the commit).
|
||||
@@ -0,0 +1,129 @@
|
||||
#!/usr/bin/env bash
|
||||
# GUARD FILE — docs/plans/panel-redesign.md § Enforcement, "design-first".
|
||||
#
|
||||
# Checks every commit of a push or pull request, one by one:
|
||||
#
|
||||
# 1. Design first. A commit that touches the accepted design — the mockups
|
||||
# under docs/assets/panel-redesign/panel/, the guard tests, this script —
|
||||
# touches nothing else under internal/web/. The guards are not adjusted
|
||||
# in the commit they are supposed to judge.
|
||||
# 2. The ratchet. internal/web/view/legacy_pages.txt only shrinks: a commit's
|
||||
# list is a subset of its parent's. A page that was restyled stays held
|
||||
# to its mockup; its name cannot be put back.
|
||||
# 3. No blob from the past. A template a commit adds or changes is not,
|
||||
# whitespace aside, a template that already existed anywhere in the
|
||||
# history before it. Taking an old page out of git is not a redesign.
|
||||
#
|
||||
# Usage: design-first.sh <base> <head> (commits base..head are checked)
|
||||
# Needs the full history: actions/checkout with fetch-depth: 0.
|
||||
set -euo pipefail
|
||||
|
||||
base="${1:?usage: design-first.sh <base> <head>}"
|
||||
head="${2:?usage: design-first.sh <base> <head>}"
|
||||
|
||||
RATCHET=internal/web/view/legacy_pages.txt
|
||||
TEMPLATES=internal/web/view/templates/
|
||||
ZERO=0000000000000000000000000000000000000000
|
||||
|
||||
# A new branch, or a force push whose old tip is gone, has no usable base:
|
||||
# check the tip commit alone rather than nothing.
|
||||
if [ "$base" = "$ZERO" ] || ! git cat-file -e "$base^{commit}" 2>/dev/null; then
|
||||
base="$(git rev-parse "$head^" 2>/dev/null || true)"
|
||||
fi
|
||||
|
||||
if [ -n "$base" ]; then
|
||||
commits="$(git rev-list --reverse --no-merges "$base..$head")"
|
||||
else
|
||||
commits="$(git rev-list --reverse --no-merges "$head")"
|
||||
fi
|
||||
|
||||
is_guard() {
|
||||
case "$1" in
|
||||
docs/assets/panel-redesign/panel/*) return 0 ;;
|
||||
.github/scripts/design-first.sh) return 0 ;;
|
||||
internal/web/guard_*_test.go | internal/web/*/guard_*_test.go) return 0 ;;
|
||||
esac
|
||||
return 1
|
||||
}
|
||||
|
||||
entries() { # the ratchet's entries at a commit, sorted; nothing if the file is absent
|
||||
git show "$1:$RATCHET" 2>/dev/null | tr -d '\r' | sed -e 's/#.*//' -e 's/[[:space:]]//g' -e '/^$/d' | sort -u
|
||||
}
|
||||
|
||||
normalised() { # a blob's hash with all whitespace removed
|
||||
git cat-file blob "$1" | tr -d ' \t\r\n' | git hash-object --stdin
|
||||
}
|
||||
|
||||
cache="$(mktemp -d)"
|
||||
trap 'rm -rf "$cache"' EXIT
|
||||
|
||||
past_hash() { # normalised hash of a historical blob, computed once
|
||||
if [ ! -f "$cache/$1" ]; then
|
||||
normalised "$1" > "$cache/$1"
|
||||
fi
|
||||
cat "$cache/$1"
|
||||
}
|
||||
|
||||
fail=0
|
||||
say() {
|
||||
echo "::error::design-first: $*"
|
||||
fail=1
|
||||
}
|
||||
|
||||
for c in $commits; do
|
||||
short="$(git rev-parse --short "$c")"
|
||||
parent="$(git rev-parse --verify --quiet "$c^" || true)"
|
||||
if [ -n "$parent" ]; then
|
||||
changed="$(git diff --name-only --no-renames "$parent" "$c")"
|
||||
else
|
||||
changed="$(git ls-tree -r --name-only "$c")"
|
||||
fi
|
||||
|
||||
# 1. design first
|
||||
guards="" impl=""
|
||||
while IFS= read -r f; do
|
||||
[ -n "$f" ] || continue
|
||||
if is_guard "$f"; then
|
||||
guards="$guards $f"
|
||||
elif [ "${f#internal/web/}" != "$f" ]; then
|
||||
impl="$impl $f"
|
||||
fi
|
||||
done <<< "$changed"
|
||||
if [ -n "$guards" ] && [ -n "$impl" ]; then
|
||||
say "$short changes the design or its guards ($(echo $guards | cut -c1-200)) together with the implementation ($(echo $impl | cut -c1-200)). A design change is its own 'design:' commit, made and accepted first."
|
||||
fi
|
||||
|
||||
# 2. the ratchet
|
||||
if [ -n "$parent" ] && grep -qx "$RATCHET" <<< "$changed"; then
|
||||
# Once the file has existed, a parent without it means an empty list.
|
||||
if git cat-file -e "$parent:$RATCHET" 2>/dev/null || [ -n "$(git log -1 --format=%h "$parent" -- "$RATCHET")" ]; then
|
||||
added="$(comm -13 <(entries "$parent") <(entries "$c"))"
|
||||
if [ -n "$added" ]; then
|
||||
say "$short adds to $RATCHET: $(echo $added). The list only shrinks — what was held to the design stays held."
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
|
||||
# 3. no blob from the past
|
||||
if [ -n "$parent" ]; then
|
||||
touched="$(git diff --name-only --no-renames --diff-filter=AM "$parent" "$c" -- "$TEMPLATES")"
|
||||
if [ -n "$touched" ]; then
|
||||
past="$(git log --format= --raw --no-abbrev --no-renames "$parent" -- "$TEMPLATES" | awk '$4 != "'$ZERO'" {print $4}' | sort -u)"
|
||||
while IFS= read -r f; do
|
||||
[ -n "$f" ] || continue
|
||||
now="$(normalised "$(git rev-parse "$c:$f")")"
|
||||
for blob in $past; do
|
||||
if [ "$(past_hash "$blob")" = "$now" ]; then
|
||||
say "$short: $f is, whitespace aside, a template that already existed in history (blob ${blob:0:12}). An old template is not a redesigned page."
|
||||
break
|
||||
fi
|
||||
done
|
||||
done <<< "$touched"
|
||||
fi
|
||||
fi
|
||||
done
|
||||
|
||||
if [ "$fail" -ne 0 ]; then
|
||||
exit 1
|
||||
fi
|
||||
echo "design-first: $(echo "$commits" | grep -c . || true) commit(s) checked, nothing to report"
|
||||
@@ -11,9 +11,9 @@ name: release
|
||||
# own native runner; a merge job then combines them into the one manifest tag
|
||||
# documented in deploy/docker-compose.yml. "test, then push" (not push-by-digest
|
||||
# then test) is deliberate: it means the bytes that get tagged are exactly the
|
||||
# bytes that passed e2e. Per-arch tags are pushed only so imagetools can
|
||||
# assemble the multi-arch manifest; merge removes them from GHCR so operators
|
||||
# see a single version tag.
|
||||
# bytes that passed e2e. The per-arch tags stay in GHCR next to the version
|
||||
# tag: they are the images the multi-arch manifest points at, and GHCR cannot
|
||||
# drop a tag without deleting the image behind it.
|
||||
on:
|
||||
release:
|
||||
types: [published]
|
||||
@@ -177,28 +177,35 @@ jobs:
|
||||
backoff=$((backoff * 2))
|
||||
done
|
||||
|
||||
- name: Remove per-arch tags from GHCR
|
||||
# Side-effect tags for imagetools assembly only — not part of the public
|
||||
# version surface (deploy/docker-compose.yml pins X.Y.Z, not X.Y.Z-amd64).
|
||||
# imagetools has no "rm" subcommand; delete via the GitHub Packages API.
|
||||
- name: Remove orphaned untagged versions from GHCR
|
||||
# Re-publishing a version moves its tags to new manifests and leaves the
|
||||
# old ones behind as untagged "sha256:..." package versions. Delete only
|
||||
# those that no tagged manifest references: the images behind a
|
||||
# multi-arch tag are package versions too, and deleting one breaks pulls
|
||||
# of that tag. Any failed lookup aborts the step before anything is
|
||||
# deleted.
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
version="${{ needs.prepare.outputs.version }}"
|
||||
image="ghcr.io/${{ github.repository }}"
|
||||
owner="${{ github.repository_owner }}"
|
||||
pkg="${{ github.event.repository.name }}"
|
||||
api="/users/${owner}/packages/container/${pkg}/versions"
|
||||
for suffix in amd64 arm64; do
|
||||
tag="${version}-${suffix}"
|
||||
mapfile -t ids < <(gh api "$api" --paginate \
|
||||
--jq ".[] | select([.metadata.container.tags[]] | index(\"${tag}\")) | .id")
|
||||
if [ "${#ids[@]}" -eq 0 ]; then
|
||||
echo "no GHCR package version for tag ${tag}"
|
||||
versions=$(gh api "${api}?per_page=100" --paginate \
|
||||
--jq '.[] | [.id, .name, (.metadata.container.tags | length)] | @tsv')
|
||||
keep=$(mktemp)
|
||||
while IFS=$'\t' read -r id digest ntags; do
|
||||
[ "$ntags" -gt 0 ] || continue
|
||||
echo "$digest" >> "$keep"
|
||||
raw=$(docker buildx imagetools inspect --raw "${image}@${digest}")
|
||||
jq -r '.manifests[]?.digest' <<<"$raw" >> "$keep"
|
||||
done <<<"$versions"
|
||||
while IFS=$'\t' read -r id digest ntags; do
|
||||
[ "$ntags" -eq 0 ] || continue
|
||||
if grep -qxF "$digest" "$keep"; then
|
||||
continue
|
||||
fi
|
||||
for id in "${ids[@]}"; do
|
||||
echo "deleting GHCR package version ${id} (tag ${tag})"
|
||||
gh api -X DELETE "${api}/${id}"
|
||||
done
|
||||
done
|
||||
echo "deleting orphaned GHCR package version ${id} (${digest})"
|
||||
gh api -X DELETE "${api}/${id}"
|
||||
done <<<"$versions"
|
||||
|
||||
@@ -10,6 +10,15 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
# design-first.sh reads each commit's parent and the template history.
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Design first
|
||||
env:
|
||||
BASE: ${{ github.event.pull_request.base.sha || github.event.before }}
|
||||
HEAD: ${{ github.event.pull_request.head.sha || github.sha }}
|
||||
run: bash .github/scripts/design-first.sh "$BASE" "$HEAD"
|
||||
|
||||
- uses: actions/setup-go@v5
|
||||
with:
|
||||
|
||||
@@ -5,6 +5,479 @@ Format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/); version
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
## [2.0.0] - 2026-10-11
|
||||
|
||||
The panel redesign. A breaking release: every panel URL changed and the
|
||||
database starts from zero - a 1.x `/data` is not upgraded (see Changed and
|
||||
Removed).
|
||||
|
||||
### Added
|
||||
|
||||
- panel-redesign (stage 0): Bulma 1.0.4 and a 57-icon subset of Tabler Icons
|
||||
3.49.0 are vendored under `/static` (MIT, listed in NOTICE). Nothing links
|
||||
them yet; the old panel is unchanged.
|
||||
- panel-redesign (stage 0): the design contract as guard tests
|
||||
(`guard_*_test.go` under `internal/web`) and a `design-first` CI step. The
|
||||
tests read their rules from the accepted mockups — class vocabulary,
|
||||
stylesheet, page structure, component outlines, the 2.0 route table — and
|
||||
pin the vendored assets. `internal/web/view/legacy_pages.txt` lists what is
|
||||
still pre-2.0 and may only shrink; CI also rejects a commit that changes a
|
||||
guard together with the implementation, or brings back a template from
|
||||
history. The kit and route tests stay skipped until their stages land.
|
||||
- panel-redesign (stage 0): the component kit. `panel.css` is the mockups'
|
||||
stylesheet rule for rule; `components.html` holds every partial with its
|
||||
typed input in `components.go`; the new `layout.html` is the shell (brick
|
||||
navbar with the wordmark, perforated edge, sibling strip, user menu,
|
||||
footer) and the signed-out screen; `GET /server/components` (global role)
|
||||
renders every partial in every state. `@kit` is off the ratchet: the kit
|
||||
is held to the design contract from here on.
|
||||
- panel-redesign (design): Sign out in the user menu is a POST form in the
|
||||
mockups too, and its button takes the same hover, focus and press tint as
|
||||
Account and Help (owner, 2026-10-09).
|
||||
|
||||
- panel-redesign (stage 1): the 2.0 schema. `users.email` is the user's own
|
||||
address; the default DMARC report address is a user's setting
|
||||
(`dmarc_default_mode` / `dmarc_default_address`) and a domain either has an
|
||||
address of its own or follows a named user's default
|
||||
(`domains.dmarc_rua_user_id`) — a new domain follows whoever created it,
|
||||
and deleting that user leaves it with no report address (replaced before
|
||||
release — see the entry of 2026-10-10 below).
|
||||
`user_inbound_domains` and the per-user `all_domains` /
|
||||
`all_inbound_domains` flags are in the schema for the inbound delegation
|
||||
that follows. Details: `docs/schema-migrations.md`.
|
||||
- panel-redesign (stage 1): inbound domains are delegated to domain
|
||||
administrators, separately from outbound. The user form has two lists, each
|
||||
with *All* (which includes domains added later; with it ticked the rows
|
||||
under it are ignored); a domain administrator needs at least one assignment
|
||||
on either. Such a user sees the inbound domains assigned to them — the list
|
||||
is filtered in the query — and can change their upstream and recipients;
|
||||
adding and deleting an inbound domain stays with the global role. Another
|
||||
tenant's domain, a missing one and a user with no inbound reach all get the
|
||||
same 404. A menu group with nothing assigned is absent, and `/` leads an
|
||||
inbound-only administrator to Inbound.
|
||||
- panel-redesign (stage 1): **Account** (`/account`, every role) and
|
||||
**Server › Settings** (`/server/settings`, global role) replace the one
|
||||
Settings page. Account has three forms, each its own POST: profile
|
||||
(username, e-mail), password, and the default DMARC report address
|
||||
(SelfPost hosted, my account e-mail, another address, none) (replaced before
|
||||
release — see the entry of 2026-10-10 below). Saving the profile no longer
|
||||
asks for the password; changing the password still does.
|
||||
- panel-redesign (stage 1): the application form is one POST. Who the
|
||||
application may send as, its client-IP allow-list and its rate limit
|
||||
("use the domain limit", manual or auto) are validated together and saved
|
||||
together or not at all — also at creation, so an application can be added
|
||||
with its limit already in place. The page that shows a generated password
|
||||
is the response to its POST only and is sent with `Cache-Control: no-store`.
|
||||
- panel-redesign (stage 1): **Server › Health** (`/server/health`) holds the
|
||||
tables of the old Status page and its two actions, *Re-check DNS* and
|
||||
*Reload configuration*. **Overview** (`/overview`) is reduced to one
|
||||
verdict per check — machine, processes, TLS certificate, queue, milter
|
||||
sockets, reverse DNS — each leading to its table on Health; a check that
|
||||
could not run shows as a warning, never as fine. Both read the same checks.
|
||||
- panel-redesign (stage 2): **Sign in** and **Create administrator** are the
|
||||
first pages in the new design - the split brand / form screen, built from
|
||||
the component kit. Sign in names the mail host above its title; a refused
|
||||
attempt is shown as a notice above the fields. `login` and `setup` are off
|
||||
the ratchet.
|
||||
- panel-redesign (design): the sign-in page before any administrator exists
|
||||
is a drawn screen (`login-no-admin`): the heading *No administrator yet* and
|
||||
the pointer to the one-time setup link, in the same column as the form. The
|
||||
signed-out column is sized through `.sp-form`, not through `form` (owner,
|
||||
2026-10-09).
|
||||
- panel-redesign (design): the Notifications boxes are out of the Account
|
||||
and Server › Settings mockups. Event notifications are not built
|
||||
(panel-notifications is a candidate), and 2.0 carries no box for a feature
|
||||
that does not exist; they are drawn again as the first step of that
|
||||
feature (owner, 2026-10-10).
|
||||
- panel-redesign (stage 2): **Overview**, **Server › Health**, **Account**
|
||||
and **Server › Settings** are in the new design. Overview shows the verdict
|
||||
as a stamp with one sentence, the six checks as cards and both domain
|
||||
lists; Health holds the tables behind the cards. Both refresh themselves.
|
||||
The Queue card reads "Empty" or "N queued". `status` and `settings` are
|
||||
off the ratchet.
|
||||
- panel-redesign (stage 2): the **Outbound** domain pages are in the new
|
||||
design: the domain list, the domain page (DNS records with what to
|
||||
publish and what is in DNS now, applications, connection), **Domain
|
||||
settings** as a page of its own (report address, rate limit, export,
|
||||
delete), the application form, the password shown once and the delete
|
||||
confirmation. `dashboard`, `domain_detail` and `domain_delete` are off
|
||||
the ratchet.
|
||||
- panel-redesign (stage 2): the **Outbound log**, the message page (facts,
|
||||
history, the lines of `mail.log`) and the three **DMARC report** pages are
|
||||
in the new design. The log refreshes its rows without touching the filter
|
||||
being chosen. `deliveries`, `delivery`, `dmarc`, `dmarc_domain` and
|
||||
`dmarc_report` are off the ratchet.
|
||||
- panel-redesign (stage 2): the **Inbound** pages are in the new design: the
|
||||
domain list, the domain page (MX record, upstream, valid recipients) and
|
||||
the delete confirmation. Every inbound domain page has a **Spam filter**
|
||||
box and the list names the filter's state: the panel reads
|
||||
`INBOUND_ANTISPAM_MILTER` and `INBOUND_ANTISPAM_MILTER_ACTION` and says
|
||||
whether inbound mail is filtered and what happens while the filter is
|
||||
down. The filter's address is shown to the global role only. `inbound`,
|
||||
`inbound_domain` and `inbound_delete` are off the ratchet.
|
||||
- panel-redesign (stage 2): the **Server** pages - System log, Backup,
|
||||
Users, the user form and its delete confirmation - and **Help** are in the
|
||||
new design. Help is a page of topics that the `?` in a box's head opens;
|
||||
the help drawer is gone. The user form sets the user's e-mail. The backup
|
||||
download is encrypted unless the box is unticked. Only the queue page is
|
||||
left on the ratchet.
|
||||
- panel-redesign (design, stage 3): **Help** covers every section -
|
||||
seventeen topics for Overview, Outbound, Inbound, Server and Account - and
|
||||
the boxes of nearly every page carry a `?` that opens their topic (owner,
|
||||
2026-10-10). A reader sees the topics of the sections their menu shows.
|
||||
- panel-redesign (design, stage 3): **a domain's DMARC report address is one
|
||||
of three choices** - no reports (a new domain), the SelfPost hosted
|
||||
address, or an address typed for the domain - set in Domain settings, with
|
||||
a button that fills in one's own profile e-mail. The per-user default
|
||||
report address, a domain "following" a user and the DMARC box on Account
|
||||
are gone, and with them `users.dmarc_default_mode`,
|
||||
`users.dmarc_default_address` and `domains.dmarc_rua_user_id` (owner,
|
||||
2026-10-10; replaces the design of 2026-09-21 described above).
|
||||
- panel-redesign (stage 3): with the inbound spam filter on, its socket is one
|
||||
of the milter sockets on **Server › Health** and counts in Overview's card.
|
||||
The panel connects to the configured address (`inet:host:port` or
|
||||
`unix:/path`, one second at most, nothing sent). A filter that does not
|
||||
answer is an error with `INBOUND_ANTISPAM_MILTER_ACTION=tempfail` - inbound
|
||||
mail is deferred - and a warning otherwise - mail goes through unfiltered.
|
||||
- **Port 25 no longer accepts `dmarc-reports@<hostname>`**, the report
|
||||
mailbox of the server as a whole. Only an address a domain has chosen is
|
||||
accepted: its hosted address `dmarc-reports+<domain>@<hostname>`, or a
|
||||
typed address that is on this host. A domain whose published `rua=` still
|
||||
names the general mailbox must be set to *SelfPost hosted* and its DMARC
|
||||
record republished (owner, 2026-10-10).
|
||||
- panel-redesign (design, stage 3): **New password** and **Delete** are in
|
||||
each row of a domain's Applications table again, as POST forms that look
|
||||
like the links beside them.
|
||||
- panel-redesign (stage 2): the **Outbound queue** page is in the new shell
|
||||
with its content unchanged - the listing of `postqueue -p` and how
|
||||
retries work. With it every page is built from the component kit: the old
|
||||
stylesheet, the old layout and the engine's second rendering path are
|
||||
deleted, and the ratchet is empty.
|
||||
|
||||
### Changed
|
||||
|
||||
- **Breaking — every panel URL changed.** A path now reads like the menu
|
||||
(`/overview`, `/outbound/…`, `/inbound/…`, `/server/…`, `/account`); the
|
||||
old paths are removed, not redirected — see *Removed*. Polled fragments all
|
||||
answer `<page>/fragment`. Everything under `/server/` is guarded for the
|
||||
global role where the routes are registered, in addition to each handler's
|
||||
own check. An application is addressed under its domain
|
||||
(`/outbound/domains/{id}/applications/{aid}`), and an application id that
|
||||
does not belong to the domain in the path is a 404.
|
||||
- **Breaking — the database starts from zero.** The 1.x migration chain
|
||||
(`0001`–`0010`) is replaced by one baseline, `0001_init.sql`. A 1.x
|
||||
`/data/selfpost.db` is refused at start-up with an explicit error and left
|
||||
untouched; nothing is migrated. A domain can be carried over with its
|
||||
export file.
|
||||
- The panel role `domain_admin` is stored as `domain`.
|
||||
- On the old domain page, "Same as Settings" makes the domain follow the
|
||||
default report address of the user who chose it (replaced before release —
|
||||
see the entry of 2026-10-10 below).
|
||||
- The old panel is untouched on screen but steps aside in the tree: its
|
||||
stylesheet is `legacy.css` and its layout `layout_legacy.html` until the
|
||||
last old page is restyled. The 2.0 menu paths in the new shell do not
|
||||
exist before stage 1; only the kit page uses that shell so far (a stage-0
|
||||
state: both files are deleted and every page is on the shell — see
|
||||
*Removed*).
|
||||
- panel-redesign is `agreed` (owner, 2026-10-09); work starts with stage 0,
|
||||
the component kit.
|
||||
- panel-redesign (stage 3): the operator guide, `architecture.md` (Panel HTTP
|
||||
surface, Persistence) and `security.md` (roles, sessions, errors shown to
|
||||
users) describe the 2.0 panel. `security.md` records two accepted risks from
|
||||
the 2.0 review: a delegated inbound upstream may be any host and port, and
|
||||
Account's profile is saved without the password.
|
||||
|
||||
### Security
|
||||
|
||||
- A panel session belongs to a user by id and ends with them. It used to be
|
||||
keyed by user name: the cookie of a deleted or renamed user stayed valid
|
||||
and became a session of whoever was next created under that name. Signing
|
||||
in again is required after upgrading (there is no upgrade from 1.x; this
|
||||
concerns development builds of 2.0).
|
||||
- Changing one's password ends one's own other sessions only; it used to
|
||||
sign every other user out. A password set by an administrator on the user
|
||||
form ends that user's sessions.
|
||||
- Internal errors (database, `saslpasswd2`, Postfix reload) are no longer
|
||||
shown on the application, inbound upstream / recipients and limit forms;
|
||||
they are logged and the page says so.
|
||||
- `/static/` no longer answers with a listing of the embedded files.
|
||||
- The DMARC reports hub is open to a domain administrator for the domains
|
||||
assigned to them, without the server's ingest statistics; the list of
|
||||
reports no longer treats "no domains" as "all domains".
|
||||
|
||||
### Fixed
|
||||
|
||||
- Saving a domain's report-address form untouched no longer moves a domain
|
||||
that follows another user's default to whoever saved it: that user's
|
||||
default is its own choice, and "My default instead" is the change
|
||||
(replaced before release — see the entry of 2026-10-10 below).
|
||||
- The auto rate-limit fields follow the limit mode on the domain and
|
||||
application forms (the script applied only the first matching rule).
|
||||
- The DMARC page of a domain no longer answers 500 once the domain has a
|
||||
reported source.
|
||||
- **The panel can read the mail queue.** The container's entrypoint left the
|
||||
queue's `public/` and `maildrop/` directories in group `postfix` on every
|
||||
start; Postfix keeps them in `postdrop`, the group `postqueue` works
|
||||
through, so the panel's `postqueue -p` was refused ("Connect to the Postfix
|
||||
showq service: Permission denied"), the Queue card said "Could not be read"
|
||||
and the queue page had no listing. Present since the persistent queue of
|
||||
1.3.0; mail flow was not affected. The e2e suite opens the queue page now.
|
||||
- **Port 25 answers again with DMARC report ingest on.** The generated
|
||||
`master.cf` gave the port-25 service `-o smtpd_recipient_restrictions=
|
||||
check_recipient_access texthash:…,…` with a space in the value; Postfix
|
||||
cuts a service line at whitespace, so `smtpd` was started with a stray
|
||||
argument and died with "fatal: unexpected command-line argument" on every
|
||||
connection - no inbound relay and no DMARC reports were accepted while
|
||||
`DMARC_REPORTS_ENABLE=true`, although `postfix check` and the suite were
|
||||
silent. The list is written with commas only, the generator refuses a
|
||||
value with whitespace, and the e2e check asks Postfix what it reads.
|
||||
Present since DMARC ingest was fitted into the 1.9.1 fix of the same
|
||||
stanza.
|
||||
- **DMARC reports are accepted with the inbound relay on.** With both
|
||||
features enabled every report address was refused as "User unknown in
|
||||
relay recipient table": the relay's unlisted-recipient check runs after the
|
||||
restriction list, and the report addresses - on the server's own name, a
|
||||
relay domain - were not in `relay_recipient_maps`. Their table is a
|
||||
recipient table now. Hidden until now by the port-25 failure above.
|
||||
- **DMARC reports reach the panel.** A report accepted on port 25 stayed in
|
||||
the queue: the generated delivery service was a bare `pipe` ("fatal:
|
||||
missing user= command-line attribute"), because the generator addressed it
|
||||
under a type Postfix does not have and set `flags=`, `user=` and `argv=`
|
||||
as `-o` parameters, which they are not. Behind that, the command would
|
||||
have refused to run (Postfix starts it without the container's
|
||||
environment) and was never told the recipient, so the check that a report
|
||||
came to the address of its own domain was skipped. The service line
|
||||
carries its arguments, the recipient is one of them, and
|
||||
`export_environment` names what the command reads. No report had been
|
||||
delivered since the feature shipped in 1.7.0; the e2e check asks Postfix
|
||||
how it reads the service.
|
||||
- Postfix no longer logs "warning: not owned by root" for the queue
|
||||
directory and its `pid/` at every start: the entrypoint hands the two back
|
||||
to root after its recursive change of owner.
|
||||
- Overview's Processes card counts the programs that are meant to stay up
|
||||
("6 of 6 running"); the idle one-shot `postfix-reload` was counted as
|
||||
running. The Milter sockets card no longer clips its second line with three
|
||||
sockets, and a DKIM value is shown whole instead of scrolling inside its
|
||||
field.
|
||||
- Server › Health shows the idle reload helper (`postfix-reload`) as
|
||||
**on demand**, with when it last ran, instead of `stopped` beside the
|
||||
running programs (owner, 2026-10-10).
|
||||
- The confirmation before deleting a user names the domains that follow
|
||||
that user's default report address and will be left without one; it used
|
||||
to say the domains were not affected (replaced before release — see the
|
||||
entry of 2026-10-10 below).
|
||||
|
||||
### Removed
|
||||
|
||||
- **2.0 starts from an empty data directory.** A 1.x `/data` is not upgraded:
|
||||
its database is refused at start and left untouched, and a 1.x backup is not
|
||||
restored; a domain is carried over with its export file. Gone with it, as
|
||||
built: the 1.x migration chain, every pre-2.0 panel path (listed next), the
|
||||
help drawer (Help is a page of topics), the default DMARC report address —
|
||||
the panel-wide one of 1.x (`dmarc_report_email`, "Same as Settings") and the
|
||||
per-user one with a domain following a user that earlier entries of this
|
||||
release describe (`users.dmarc_default_*`, `domains.dmarc_rua_user_id`) —
|
||||
together with the DMARC box on Account, the old stylesheet `legacy.css`, the
|
||||
old layout `layout_legacy.html`, and the unused `rate_limits.allowed_ips`
|
||||
column.
|
||||
- Old panel paths, GET and POST alike, without redirects: `/status`,
|
||||
`/status/fragment`, `/status/recheck`, `/reload`, `/domains` and everything
|
||||
under it (`/domains/import`, `/domains/{id}`, `…/dns-recheck`, `…/delete`,
|
||||
`…/ratelimit`, `…/ratelimit/recalc`, `…/dmarc`, `…/export`), `/inbound` and
|
||||
`/inbound/{id}/…`, `/dmarc`, `/dmarc/reports/{id}`, `/dmarc/domains/{id}`,
|
||||
`/deliveries`, `/deliveries/rows`, `/deliveries/{id}`, `/mail-queue`,
|
||||
`/mail-queue/body`, `/system-log`, `/system-log/body`, `/backup`, `/users`
|
||||
and everything under it, `/settings`, and the `/account` → `/settings`
|
||||
redirect; `POST /domains/{id}/applications` and `/applications/{aid}/mode`,
|
||||
`/authips`, `/ratelimit`, `/ratelimit/recalc`, `/password`, `/delete` — the
|
||||
three separate saves of an application are one form now.
|
||||
- The instance setting `dmarc_report_email` (a mirror of the global user's
|
||||
profile field) and the unused `rate_limits.allowed_ips` column.
|
||||
|
||||
## [1.9.5] - 2026-10-09
|
||||
|
||||
CI-only cut: the release workflow no longer breaks the multi-arch image it
|
||||
just published. No product code changes.
|
||||
|
||||
### Fixed
|
||||
|
||||
- ci (GHCR): the release no longer deletes 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 `docker pull ghcr.io/mixeme/selfpost:1.3.0`
|
||||
failed with a missing manifest. The per-arch tags now stay; the merge job
|
||||
instead removes untagged versions that no tagged manifest references.
|
||||
|
||||
## [1.9.4] - 2026-10-09
|
||||
|
||||
Plan-only cut, owner-requested: the redesign goes first as `2.0.0` with a
|
||||
component-kit stage and guards against resurrected pages; antispam moves to
|
||||
`2.1.0`; a new candidate explains the empty delivery log. No product code
|
||||
changes.
|
||||
|
||||
### Changed
|
||||
|
||||
- Roadmap order (owner, 2026-10-09): the panel redesign ships first as
|
||||
`2.0.0`; inbound-antispam-panel follows as `2.1.0` on the 2.0 baseline
|
||||
(its migration becomes `0002_inbound_spam_log.sql`), built from the
|
||||
redesign's component kit so its two screens are styled once. The redesign
|
||||
plan gains a **Component kit** section and stage (partials with typed
|
||||
inputs, one `panel.css`, a `/server/components` kit page, acceptance before
|
||||
any page is restyled), a staged checklist (5/27), Health decided inside 2.0
|
||||
and the queue pages left as old content in the new shell, and an **Against
|
||||
resurrected pages** section: a shrink-only `legacyPages` ratchet in the
|
||||
guard tests, the outline golden, the old-path test and a history-blob
|
||||
check in CI, so an old template from git history cannot pass as a
|
||||
redesigned page.
|
||||
- New roadmap candidate `delivery-log-storage`: the *Delivery log* box on a
|
||||
message's 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; the item proposes keeping them with the send-log row.
|
||||
|
||||
## [1.9.3] - 2026-09-22
|
||||
|
||||
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
|
||||
field (2.0 baseline `users.email`, today `dmarc_report_email`) — edited under
|
||||
**Account**, seen and set by the global administrator on the user form and in
|
||||
the users list; DMARC may *use* it, as one choice beside the hosted address,
|
||||
another address or none (Settings and domain settings). **Account** gains a
|
||||
Notifications box and **Settings** a switch for the new roadmap candidate
|
||||
`panel-notifications`; `password-reset` is added as a candidate too. The
|
||||
inbound spam filter becomes visible: a *Spam filter* box on every inbound
|
||||
domain, a tag on the inbound domains list, its socket among the milter
|
||||
sockets. Preflight loses a box that only filled space — the test form now
|
||||
shares its box with its result — and the plan's layout rule and contract say
|
||||
so: what stands beside a form must be real. The role is labelled by its reach,
|
||||
`global` or `domain` — everyone in the panel is an administrator — and each
|
||||
assignment list on the user form gains **All**, a per-user flag that covers
|
||||
domains added later. The DMARC default report address is a **user's**
|
||||
setting, not the server's: it moves from Server › Settings to **Account**
|
||||
with its report-authorization record, and a domain follows the default of a
|
||||
*named* user, so two people sharing a domain never pull it two ways.
|
||||
- Panel redesign, contrast: a sweep of every text node against its real ground
|
||||
found the solid red button at 3:1 (Bulma derives a solid button's text from
|
||||
the fill's lightness; brick had been patched, red had not), the muted grey
|
||||
at 4.1:1, the amber postmark at 4.0 and Bulma's menu labels at 4.2. Solid
|
||||
buttons are white on colour, muted is `#5c6774`, the postmark uses the warn
|
||||
token; all 33 screens now measure 4.5:1 or better. The sweep is kept as
|
||||
`panel/audit.js` — text size, contrast, overflow, clipped values, missing
|
||||
icons — and its clean output joins the evidence a restyle step must show.
|
||||
- Panel redesign, header (owner's choice: variant A, with the perforation on a
|
||||
light edge as on the mark): the navbar carries the **wordmark** instead of
|
||||
the stamp — `docs/assets/selfpost-wordmark.svg` and `-ink`, the stamp's own
|
||||
outlines, recorded in `docs/product.md` — and under the brick bar runs the
|
||||
stamp's paper margin with the perforation along its outer edge: round holes,
|
||||
flat teeth, the ink hairline (`panel/perf-edge.svg`). All 33 screens rebuilt;
|
||||
the components page documents the shell; the comparison sheet is removed.
|
||||
The six health cards now split into six, three or two columns, so one card
|
||||
is never left alone on a row. Three corrections after the owner's review:
|
||||
the paper edge is 8px under the 60px bar — it had been scaled from the hole
|
||||
size to 16px, where the mark's margin is a tenth of the brick field; the
|
||||
health row is compact again (71px: smaller icons, each line a block with its
|
||||
own line height, a verdict never wraps and stretches the row); and a
|
||||
read-only value is as tall as the Copy button joined to it. `check.py` no
|
||||
longer counts `min-width: 0` as setting a width. Text sizes: the filter
|
||||
controls in a box head and the navbar's dropdown entries are 15px like the
|
||||
bar itself (they had Bulma's `is-small` and its .875rem dropdown); `check.py`
|
||||
now rejects `is-small` on anything but a progress bar.
|
||||
- Panel redesign, type scale: about 40 % of the panel's text measured under
|
||||
13px (labels 11.7, table heads 10.5, fact labels 10.2, tags and help 11.3).
|
||||
Body is now 16px with two tokens under it — `--sp-fs-s` 14px for supporting
|
||||
text and `--sp-fs-xs` 13px for upper-case mono labels and tags; Bulma's
|
||||
small sizes are overridden or dropped (`sp-small` replaces `is-size-7`), and
|
||||
`check.py` fails any font size under the floor. Every table now sits in a
|
||||
`table-container` (enforced), addresses in tables break after the `@`, the
|
||||
inbound log folds the client IP under the sender; all screens fit without
|
||||
scrolling at 1280px and up. Documented on the components page and in the
|
||||
plan (§ Type and tables).
|
||||
- Panel redesign, header comparison sheet
|
||||
([docs/assets/panel-redesign/header/](docs/assets/panel-redesign/header/index.html)):
|
||||
at nav size the stamp dissolves into the brick bar and the perforated edge
|
||||
was drawn inverted (round bumps instead of round holes). Three replacements
|
||||
beside the current header — **A** wordmark with the stamp's real hole-to-tooth
|
||||
perforation bitten into the bar, **B** wordmark on a plain bar, **C** a
|
||||
stamp-paper header with an ink hairline and brick only behind the name. The
|
||||
wordmark is cut from the stamp's own outlines. Pending the owner's choice;
|
||||
the mockups are unchanged. `.claude/launch.json` serves the mockups locally
|
||||
(`python -m http.server 8765` from the repository root).
|
||||
- Panel redesign, the roadmap's screens: **Inbound › Filter lists** and the
|
||||
adjusted **Inbound › Log** (client, decision, engine and reason; *Allow /
|
||||
Deny sender*) for inbound-antispam-panel, **Inbound › Quarantine** (list and
|
||||
held message with Release / Discard) for inbound-quarantine, and **Server ›
|
||||
Preflight** for preflight — all from existing components; 33 screens. Their
|
||||
paths join § Routes; the quarantine and preflight plans link their mockups.
|
||||
A mockup is the target look, not a decision to build: each item keeps its
|
||||
status, and quarantine's open questions stay open.
|
||||
- Panel redesign, two owner decisions recorded in
|
||||
`docs/plans/panel-redesign.md`: **no compatibility** — the redesign is
|
||||
`2.0.0`, old paths are removed without redirects, the SQLite migration chain
|
||||
is replaced by one baseline and 2.0 starts from an empty data directory
|
||||
(schema-squash ships as a step of this plan, without its upgrade gate; the
|
||||
only live installation is the owner's); and
|
||||
**inbound delegation** — a domain administrator manages assigned inbound
|
||||
domains, granted separately from outbound (`user_inbound_domains`, a second
|
||||
list on the user form, per-domain authorization under `/inbound/`). Mockups `users`, `user`,
|
||||
`user-delete` redrawn for the two lists. Checklist `3/21`.
|
||||
- Panel redesign, routes: the owner decided that paths follow the new names
|
||||
and structure. `docs/plans/panel-redesign.md` § Routes maps every screen to
|
||||
its new path (`/overview`, `/outbound/…`, `/inbound/…`, `/server/…`,
|
||||
`/account`) with the rules that come along — one `<page>/fragment` name, the
|
||||
application form as a single POST, 301s from old GET paths for one MINOR,
|
||||
subtree `requireGlobal()` under security review, and
|
||||
`TestRoutesFollowNavigation`. Mockups gained what the first pass missed: the
|
||||
*Reload configuration* box (now on Server › Health), the `flash` component
|
||||
and the `sp-help` link that replaces the help drawer. Checklist `1/17`.
|
||||
- Panel redesign, design contract:
|
||||
[docs/plans/panel-redesign.md](docs/plans/panel-redesign.md) rewritten for
|
||||
the accepted direction — three layers (Bulma as shipped, adapters, own `sp-`
|
||||
components), three page layouts, what an implementer must not do, the guard
|
||||
tests that land before any template, a `design-first` CI step and
|
||||
evidence-based acceptance. A
|
||||
[components page](docs/assets/panel-redesign/panel/components.html) shows
|
||||
every building block in every state; `panel/check.py` lints the mockups
|
||||
against the contract and writes `panel/outlines.json`, the per-screen
|
||||
component skeleton an implementation is compared with. Own modifier classes
|
||||
renamed to carry the `sp-` prefix so the vocabulary is machine-checkable.
|
||||
Agent rule `.cursor/rules/panel-design.mdc`; roadmap row updated (`0/15`).
|
||||
- Panel redesign, stage 2: the accepted direction drawn on every screen —
|
||||
29 pages under [docs/assets/panel-redesign/panel/](docs/assets/panel-redesign/index.html).
|
||||
Direction C (Bulma) is the base, with A's postal signature (postmark verdict,
|
||||
numbered mono section heads, the stamp's perforated edge) and B's icons.
|
||||
Overview keeps verdicts only and the tables move to a new **Server › Health**
|
||||
page; the own password becomes **Account**; queues are drawn as tables
|
||||
(needs `postqueue -j`) and **Inbound** gets its own Log and Queue. Pages are
|
||||
built from fragments by `panel/build.py`; the built HTML is committed.
|
||||
Mockups only; nothing in `internal/web` changes.
|
||||
- Panel redesign, stage 1: three directions drawn on the three key screens
|
||||
(Overview, an outbound domain, sign-in) under
|
||||
[docs/assets/panel-redesign/](docs/assets/panel-redesign/index.html) —
|
||||
**A** Pico CSS ("Blank"), **B** Tabler ("Console"), **C** Bulma ("Counter"),
|
||||
with a cover sheet comparing size, JavaScript, CSP fit and own CSS. All three
|
||||
share a navigation grouped by mail direction (Overview · Outbound · Inbound ·
|
||||
Server, with a symmetric Log and Queue on both sides), one block per DNS
|
||||
record instead of separate "status" and "records to publish" cards, and
|
||||
applications as a table. Mockups only; nothing in `internal/web` changes.
|
||||
The brief behind them supersedes the "no component library" decision in
|
||||
`docs/plans/panel-redesign.md`; that file is rewritten once a direction is
|
||||
accepted.
|
||||
- Roadmap item and givens for a panel visual redesign
|
||||
([docs/plans/panel-redesign.md](docs/plans/panel-redesign.md)) — layout
|
||||
grammar (`measure` / `fill` plus Web Awesome layout CSS), no JS component
|
||||
library, new mockups rather than `docs/assets/panel-ui/`. Status: candidate.
|
||||
- Roadmap item and plan for `/preflight` installation check page
|
||||
([docs/plans/preflight.md](docs/plans/preflight.md)) — instance-level
|
||||
infrastructure verification (rDNS, TLS, ports, HELO banner, proxy headers,
|
||||
DKIM socket, test email). Status: candidate.
|
||||
|
||||
## [1.9.2] - 2026-09-22
|
||||
|
||||
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
|
||||
@@ -15,22 +488,8 @@ GUI, tests, documentation, AGPL compliance; 38 findings) produced most of what
|
||||
follows. Its four open items live in [roadmap.md](docs/roadmap.md); its working
|
||||
document is not part of the release.
|
||||
|
||||
### Changed
|
||||
|
||||
- README hero line and a **Positioning** block in `docs/product.md` record the
|
||||
tagline (`YOUR SMTP. YOUR RULES.`) and hero copy; the stamp subtitle stays
|
||||
`SELF-HOSTED SMTP RELAY`. `docs/assets/selfpost-proof.html` updated to match.
|
||||
|
||||
### Added
|
||||
|
||||
- Roadmap item and givens for a panel visual redesign
|
||||
([docs/plans/panel-redesign.md](docs/plans/panel-redesign.md)) — layout
|
||||
grammar (`measure` / `fill` plus Web Awesome layout CSS), no JS component
|
||||
library, new mockups rather than `docs/assets/panel-ui/`. Status: candidate.
|
||||
- Roadmap item and plan for `/preflight` installation check page
|
||||
([docs/plans/preflight.md](docs/plans/preflight.md)) — instance-level
|
||||
infrastructure verification (rDNS, TLS, ports, HELO banner, proxy headers,
|
||||
DKIM socket, test email). Status: candidate.
|
||||
- SQLite migration `0010_send_log_app_login_index.sql` —
|
||||
`idx_send_log_app_login_created_at`. The journal-milter counts an
|
||||
application's messages on every `MAIL FROM`, and the domain page computes
|
||||
@@ -46,6 +505,12 @@ document is not part of the release.
|
||||
survived three releases because the suite only ever exercised the flags-off
|
||||
path.
|
||||
|
||||
### Changed
|
||||
|
||||
- README hero line and a **Positioning** block in `docs/product.md` record the
|
||||
tagline (`YOUR SMTP. YOUR RULES.`) and hero copy; the stamp subtitle stays
|
||||
`SELF-HOSTED SMTP RELAY`. `docs/assets/selfpost-proof.html` updated to match.
|
||||
|
||||
### Fixed
|
||||
|
||||
- The setup account's password is hashed at bcrypt cost `12` like every other
|
||||
@@ -75,17 +540,12 @@ document is not part of the release.
|
||||
guard; one check now returns a context-specific message for self-demotion.
|
||||
- Settings flash text now reflects what actually changed and only mentions
|
||||
session sign-out when the password changed.
|
||||
- Login/setup rate-limiter bucket eviction now prefers the nearest-expiry bucket
|
||||
and does not evict currently blocked buckets.
|
||||
- Help page/drawer links now use `SourceURL` (`{{.SourceURL}}/blob/main/docs/...`)
|
||||
instead of a hardcoded repository URL.
|
||||
- Help drawer close controls include accessibility attributes (`role="button"`,
|
||||
`aria-label`) for the scrim and close action.
|
||||
- Password hashing now uses explicit bcrypt cost `12` in settings/user password
|
||||
updates, instead of `bcrypt.DefaultCost`.
|
||||
- Test coverage expanded for DMARC map token injection rejection and email
|
||||
validation edge cases (ASCII/local-part/domain/freemail cases).
|
||||
|
||||
- **Port 25 no longer runs the submission milter chain.** `smtpd_milters` is
|
||||
now always overridden on `smtp/inet` — to the optional antispam milter, or
|
||||
to nothing. Previously, with `INBOUND_ANTISPAM_MILTER` unset, inbound mail
|
||||
|
||||
@@ -21,6 +21,22 @@ htmx 2.0.4 (internal/web/view/static/htmx.min.js)
|
||||
Licence: Zero-Clause BSD (0BSD)
|
||||
https://github.com/bigskysoftware/htmx
|
||||
|
||||
Bulma 1.0.4 (internal/web/view/static/bulma.min.css)
|
||||
Copyright (c) 2023 Jeremy Thomas
|
||||
Licence: MIT
|
||||
https://github.com/jgthms/bulma
|
||||
Distributed unmodified.
|
||||
|
||||
Tabler Icons 3.49.0
|
||||
Copyright (c) 2020-2026 Paweł Kuna
|
||||
Licence: MIT
|
||||
https://github.com/tabler/tabler-icons
|
||||
Distributed as two files:
|
||||
internal/web/view/static/tabler-icons.css
|
||||
internal/web/view/static/tabler-icons.woff2
|
||||
Webfont subset to the 57 icons the panel uses, made with fontTools
|
||||
pyftsubset.
|
||||
|
||||
IBM Plex Sans / IBM Plex Mono
|
||||
Copyright © 2017 IBM Corp., with Reserved Font Name "Plex"
|
||||
Licence: SIL Open Font License 1.1
|
||||
|
||||
@@ -26,9 +26,9 @@ send log and DNS checks in the panel, encrypted backups.
|
||||
- Outbound SMTP (465/smtps; optional 587 submission) with per-domain DKIM signing
|
||||
- Optional inbound relay on port 25 (backup-MX / forwarder; off by default)
|
||||
- Optional DMARC aggregate report ingest on port 25 (off by default)
|
||||
- In-panel Help drawer and `/help` page for Status and domain controls
|
||||
- In-panel Help: a page of topics, opened from the `?` in the head of a box
|
||||
- Per-application client IP allow-list for SMTP AUTH (optional)
|
||||
- Web panel — domains, applications, deliveries, mail queue, system log, backup
|
||||
- Web panel grouped by mail direction — Overview, Outbound (domains, log, queue, DMARC reports), Inbound, Server (health, system log, backup, users, settings)
|
||||
- Multi-domain relay — each SASL application is bound to one sending domain
|
||||
- DNS status checks (PTR, SPF, DKIM, DMARC) with in-panel re-check
|
||||
- Two-level rate limiting — IP backstop (Postfix), per-domain ceilings, and trusted-IP app overrides
|
||||
@@ -112,7 +112,7 @@ docker run --rm -d --name selfpost-try \
|
||||
-e SELFPOST_HOSTNAME=mail.local.test \
|
||||
-e PANEL_COOKIE_SECURE=false \
|
||||
-v selfpost-try-data:/data \
|
||||
ghcr.io/mixeme/selfpost:1.9.1
|
||||
ghcr.io/mixeme/selfpost:2.0.0
|
||||
```
|
||||
|
||||
**Get the setup URL** (pick one):
|
||||
|
||||
@@ -157,5 +157,16 @@ if [ ! -d /data/postfix/queue/active ]; then
|
||||
postfix set-permissions
|
||||
fi
|
||||
chown -R postfix:postfix /data/postfix/queue
|
||||
# The recursive chown above would leave public/ and maildrop/ in group postfix.
|
||||
# Postfix keeps those two in its setgid group: postqueue and postdrop are setgid
|
||||
# postdrop and reach the showq socket and the maildrop directory through that
|
||||
# group alone. With the wrong group `postqueue -p` run by the panel answers
|
||||
# "Connect to the Postfix showq service: Permission denied" and the panel cannot
|
||||
# read the queue (the Queue card and the Outbound queue page).
|
||||
chgrp postdrop /data/postfix/queue/public /data/postfix/queue/maildrop
|
||||
# It would also leave the queue directory itself and pid/ owned by postfix.
|
||||
# Postfix keeps those two owned by root (master, which runs as root, is the only
|
||||
# writer) and logs "warning: not owned by root" for each at every start.
|
||||
chown root /data/postfix/queue /data/postfix/queue/pid
|
||||
|
||||
exec /usr/bin/supervisord -c /etc/supervisor/supervisord.conf
|
||||
|
||||
@@ -13,12 +13,25 @@
|
||||
# a fixed literal or a container environment variable.
|
||||
set -eu
|
||||
|
||||
# postconf -P refuses master.cf -o values that contain whitespace; inbound port
|
||||
# 25 needs restriction lists and milter specs that include spaces. Patch the
|
||||
# smtp/inet service stanza directly instead.
|
||||
# Sets one -o option of the smtp/inet (port 25) service by patching its stanza.
|
||||
#
|
||||
# A master.cf -o value must not contain whitespace: master splits the line on
|
||||
# it, so "-o name=check_recipient_access texthash:/x,reject" reaches smtpd as
|
||||
# the option "name=check_recipient_access" plus a stray argument, and smtpd dies
|
||||
# at start with "fatal: unexpected command-line argument" - port 25 then
|
||||
# answers nobody. (postconf -P refuses such values for the same reason.) Postfix
|
||||
# splits a restriction list on commas as well as on whitespace, so every value
|
||||
# passed here is written with commas only; a value with whitespace is a bug in
|
||||
# this script and stops the container.
|
||||
master_cf_set_smtp_inet_o() {
|
||||
param="$1"
|
||||
value="$2"
|
||||
case "$value" in
|
||||
*[[:space:]]*)
|
||||
echo "FATAL: master.cf -o ${param} would contain whitespace: ${value}" >&2
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
awk -v p="$param" -v v="$value" '
|
||||
/^smtp[ \t]+inet[ \t]/ && !done {
|
||||
print
|
||||
@@ -272,11 +285,21 @@ if [ "${INBOUND_RELAY_ENABLE}" = "true" ] || [ "${DMARC_REPORTS_ENABLE}" = "true
|
||||
[ -e "$f" ] || : > "$f"
|
||||
done
|
||||
ln -sf /usr/local/bin/panel /usr/local/bin/dmarc-ingest
|
||||
postconf -M "dmarc-ingest/unix-pipe=dmarc-ingest unix - n n - - pipe"
|
||||
postconf -P \
|
||||
"dmarc-ingest/unix-pipe/flags=Rq" \
|
||||
"dmarc-ingest/unix-pipe/user=panel" \
|
||||
"dmarc-ingest/unix-pipe/argv=/usr/local/bin/dmarc-ingest"
|
||||
# flags=, user= and argv= are arguments of pipe(8), not -o parameters:
|
||||
# postconf -P refuses them ("unmatched service_name/type") and leaves a
|
||||
# bare "pipe", which dies with "fatal: missing user= command-line
|
||||
# attribute" on every report. They go into the service line itself.
|
||||
# ${recipient} is expanded by pipe(8), not by the shell: the command
|
||||
# learns which report address the message came to from its argument.
|
||||
postconf -M 'dmarc-ingest/unix=dmarc-ingest unix - n n - - pipe flags=Rq user=panel argv=/usr/local/bin/dmarc-ingest ${recipient}'
|
||||
# One recipient per run, so ${recipient} is one argument.
|
||||
postconf -e "dmarc-ingest_destination_recipient_limit=1"
|
||||
# pipe(8) starts the command with export_environment only, so what the
|
||||
# command reads from the container's environment is named there.
|
||||
DMARC_INGEST_ENV="DMARC_REPORTS_ENABLE=true"
|
||||
[ -z "${SELFPOST_DATA_DIR:-}" ] || DMARC_INGEST_ENV="${DMARC_INGEST_ENV} SELFPOST_DATA_DIR=${SELFPOST_DATA_DIR}"
|
||||
[ -z "${SELFPOST_DB_PATH:-}" ] || DMARC_INGEST_ENV="${DMARC_INGEST_ENV} SELFPOST_DB_PATH=${SELFPOST_DB_PATH}"
|
||||
postconf -e "export_environment=TZ MAIL_CONFIG LANG ${DMARC_INGEST_ENV}"
|
||||
if [ -n "$RELAY_DOMAINS_SETTING" ]; then
|
||||
RELAY_DOMAINS_SETTING="${RELAY_DOMAINS_SETTING} texthash:${DMARC_RELAY_DOMAINS_MAP}"
|
||||
TRANSPORT_SETTING="${TRANSPORT_SETTING} texthash:${DMARC_TRANSPORT_MAP}"
|
||||
@@ -284,15 +307,31 @@ if [ "${INBOUND_RELAY_ENABLE}" = "true" ] || [ "${DMARC_REPORTS_ENABLE}" = "true
|
||||
RELAY_DOMAINS_SETTING="texthash:${DMARC_RELAY_DOMAINS_MAP}"
|
||||
TRANSPORT_SETTING="texthash:${DMARC_TRANSPORT_MAP}"
|
||||
fi
|
||||
RECIPIENT_RESTRICTIONS="check_recipient_access texthash:${DMARC_RECIPIENTS_MAP},${RECIPIENT_RESTRICTIONS}"
|
||||
# Comma, not a space, between check_recipient_access and its table: see
|
||||
# master_cf_set_smtp_inet_o.
|
||||
RECIPIENT_RESTRICTIONS="check_recipient_access,texthash:${DMARC_RECIPIENTS_MAP},${RECIPIENT_RESTRICTIONS}"
|
||||
else
|
||||
postconf -MX "dmarc-ingest/unix" 2>/dev/null || true
|
||||
postconf -X dmarc-ingest_destination_recipient_limit export_environment 2>/dev/null || true
|
||||
fi
|
||||
postconf -e \
|
||||
"relay_domains=${RELAY_DOMAINS_SETTING}" \
|
||||
"transport_maps=${TRANSPORT_SETTING}" \
|
||||
"smtpd_reject_unlisted_recipient=yes"
|
||||
if [ "${INBOUND_RELAY_ENABLE}" = "true" ]; then
|
||||
# With the relay on, Postfix refuses every recipient of a relay domain
|
||||
# that relay_recipient_maps does not list (smtpd_reject_unlisted_recipient
|
||||
# above) - and it does so after the restriction list, whatever
|
||||
# check_recipient_access answered. The DMARC report addresses live on a
|
||||
# relay domain too (the server's own name), so their table is a recipient
|
||||
# table as well; without it they are "User unknown in relay recipient
|
||||
# table" and no report is ever accepted while both features are on.
|
||||
RELAY_RECIPIENT_SETTING="texthash:${RELAY_RECIPIENTS_MAP}"
|
||||
if [ "${DMARC_REPORTS_ENABLE}" = "true" ]; then
|
||||
RELAY_RECIPIENT_SETTING="${RELAY_RECIPIENT_SETTING} texthash:${DMARC_RECIPIENTS_MAP}"
|
||||
fi
|
||||
postconf -e \
|
||||
"relay_recipient_maps=texthash:${RELAY_RECIPIENTS_MAP}" \
|
||||
"relay_recipient_maps=${RELAY_RECIPIENT_SETTING}" \
|
||||
"smtp_tls_policy_maps=texthash:${TLS_POLICY_MAP}"
|
||||
else
|
||||
postconf -e \
|
||||
@@ -355,7 +394,8 @@ if [ "${INBOUND_RELAY_ENABLE}" = "true" ] || [ "${DMARC_REPORTS_ENABLE}" = "true
|
||||
master_cf_set_smtp_inet_o smtpd_milters "${INBOUND_MILTERS}"
|
||||
else
|
||||
postconf -MX "smtp/inet" 2>/dev/null || true
|
||||
postconf -MX "dmarc-ingest/unix-pipe" 2>/dev/null || true
|
||||
postconf -MX "dmarc-ingest/unix" 2>/dev/null || true
|
||||
postconf -X dmarc-ingest_destination_recipient_limit export_environment 2>/dev/null || true
|
||||
postconf -e \
|
||||
"relay_domains=" \
|
||||
"transport_maps=" \
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
package main
|
||||
|
||||
import "testing"
|
||||
|
||||
// The panel reads the inbound filter's two variables with the semantics of
|
||||
// build/postfix-config.sh: the action is accept unless it says tempfail, and a
|
||||
// value the script would refuse is not guessed at.
|
||||
func TestAntispamActionMirrorsTheBuildScript(t *testing.T) {
|
||||
for raw, want := range map[string]string{
|
||||
"": "accept", "accept": "accept", "tempfail": "tempfail",
|
||||
"reject": "", "Accept": "", "TEMPFAIL": "", " accept": "",
|
||||
} {
|
||||
if got := antispamAction(raw); got != want {
|
||||
t.Errorf("antispamAction(%q) = %q, want %q", raw, got, want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestLoadConfigReadsTheInboundFilter(t *testing.T) {
|
||||
t.Setenv("INBOUND_RELAY_ENABLE", "true")
|
||||
t.Setenv("INBOUND_ANTISPAM_MILTER", "inet:antispam:11332")
|
||||
t.Setenv("INBOUND_ANTISPAM_MILTER_ACTION", "tempfail")
|
||||
cfg := loadConfig()
|
||||
if !cfg.inboundEnabled || cfg.inboundAntispamMilter != "inet:antispam:11332" || cfg.inboundAntispamAction != "tempfail" {
|
||||
t.Errorf("config = %+v", cfg)
|
||||
}
|
||||
|
||||
t.Setenv("INBOUND_ANTISPAM_MILTER", "")
|
||||
t.Setenv("INBOUND_ANTISPAM_MILTER_ACTION", "")
|
||||
cfg = loadConfig()
|
||||
if cfg.inboundAntispamMilter != "" || cfg.inboundAntispamAction != "accept" {
|
||||
t.Errorf("unset filter: milter %q, action %q", cfg.inboundAntispamMilter, cfg.inboundAntispamAction)
|
||||
}
|
||||
}
|
||||
@@ -26,10 +26,7 @@ func runDMARCIngestMode() error {
|
||||
}
|
||||
defer st.Close()
|
||||
|
||||
recipient := strings.ToLower(strings.TrimSpace(os.Getenv("RECIPIENT")))
|
||||
if recipient == "" {
|
||||
recipient = strings.ToLower(strings.TrimSpace(os.Getenv("ORIGINAL_RECIPIENT")))
|
||||
}
|
||||
recipient := ingestRecipient(os.Args[1:])
|
||||
if err := dmarc.IngestMessage(st, io.LimitReader(os.Stdin, maxIngestStdin), recipient, time.Now().UTC()); err != nil {
|
||||
if incrErr := st.IncrDMARCParseFailures(); incrErr != nil {
|
||||
log.Printf("dmarc ingest: record failure: %v", incrErr)
|
||||
@@ -39,6 +36,21 @@ func runDMARCIngestMode() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// ingestRecipient is the report address the message was delivered to. Postfix's
|
||||
// pipe(8) hands it over as an argument (${recipient} in the service's argv,
|
||||
// build/postfix-config.sh) - it sets no RECIPIENT variable, that is local(8).
|
||||
// Without it the check that a report came to its own domain's address is
|
||||
// skipped.
|
||||
func ingestRecipient(args []string) string {
|
||||
for _, a := range args {
|
||||
if a == "-dmarc-ingest" {
|
||||
continue
|
||||
}
|
||||
return strings.ToLower(strings.TrimSpace(a))
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func isDMARCIngestInvocation() bool {
|
||||
if len(os.Args) > 1 && os.Args[1] == "-dmarc-ingest" {
|
||||
return true
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
package main
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestIngestRecipient(t *testing.T) {
|
||||
for _, tc := range []struct {
|
||||
args []string
|
||||
want string
|
||||
}{
|
||||
// As pipe(8) starts it: the symlink name, then ${recipient}.
|
||||
{[]string{"dmarc-reports+Example.org@mail.example.org"}, "dmarc-reports+example.org@mail.example.org"},
|
||||
{[]string{"-dmarc-ingest", "dmarc-reports+example.org@mail.example.org"}, "dmarc-reports+example.org@mail.example.org"},
|
||||
{[]string{"-dmarc-ingest"}, ""},
|
||||
{nil, ""},
|
||||
} {
|
||||
if got := ingestRecipient(tc.args); got != tc.want {
|
||||
t.Errorf("ingestRecipient(%q) = %q, want %q", tc.args, got, tc.want)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -89,6 +89,8 @@ var loadConfigKeys = []string{
|
||||
"POSTFIX_DIR",
|
||||
"SELFPOST_DEPLOY_ROOT",
|
||||
"INBOUND_RELAY_ENABLE",
|
||||
"INBOUND_ANTISPAM_MILTER",
|
||||
"INBOUND_ANTISPAM_MILTER_ACTION",
|
||||
"DMARC_REPORTS_ENABLE",
|
||||
}
|
||||
|
||||
|
||||
@@ -110,6 +110,8 @@ func newPanel(cfg config, st *store.Store) (*web.Server, error) {
|
||||
RateLimitWindowSeconds: cfg.rateLimitWindowSeconds,
|
||||
RetryPolicy: retryPolicy,
|
||||
InboundEnabled: cfg.inboundEnabled,
|
||||
InboundAntispamMilter: cfg.inboundAntispamMilter,
|
||||
InboundAntispamAction: cfg.inboundAntispamAction,
|
||||
DMARCEnabled: cfg.dmarcEnabled,
|
||||
SendLogRetentionEnvDefault: cfg.retentionDays,
|
||||
}, cfg.setupTokenPath)
|
||||
|
||||
@@ -91,7 +91,12 @@ type config struct {
|
||||
deployRoot string
|
||||
|
||||
inboundEnabled bool
|
||||
dmarcEnabled bool
|
||||
// The optional milter on the inbound listener (INBOUND_ANTISPAM_MILTER) and
|
||||
// what Postfix does when it is down. Read-only: build/postfix-config.sh is
|
||||
// what attaches it; the panel only says so. See antispamAction.
|
||||
inboundAntispamMilter string
|
||||
inboundAntispamAction string
|
||||
dmarcEnabled bool
|
||||
}
|
||||
|
||||
func loadConfig() config {
|
||||
@@ -154,12 +159,28 @@ func loadConfig() config {
|
||||
postfixDir: envDefault("POSTFIX_DIR", filepath.Join(dataDir, "postfix")),
|
||||
deployRoot: envDefault("SELFPOST_DEPLOY_ROOT", "/selfpost-deploy"),
|
||||
// Optional inbound relay (backup-MX / forwarder). Off unless exactly "true".
|
||||
inboundEnabled: os.Getenv("INBOUND_RELAY_ENABLE") == "true",
|
||||
inboundEnabled: os.Getenv("INBOUND_RELAY_ENABLE") == "true",
|
||||
inboundAntispamMilter: os.Getenv("INBOUND_ANTISPAM_MILTER"),
|
||||
inboundAntispamAction: antispamAction(os.Getenv("INBOUND_ANTISPAM_MILTER_ACTION")),
|
||||
// Optional DMARC aggregate ingest on port 25. Off unless exactly "true".
|
||||
dmarcEnabled: os.Getenv("DMARC_REPORTS_ENABLE") == "true",
|
||||
}
|
||||
}
|
||||
|
||||
// antispamAction reads INBOUND_ANTISPAM_MILTER_ACTION the way
|
||||
// build/postfix-config.sh does: empty means accept, and accept and tempfail are
|
||||
// the only other values it lets the container start with. Anything else is
|
||||
// returned as "" — the panel says nothing about the action rather than guess.
|
||||
func antispamAction(raw string) string {
|
||||
switch raw {
|
||||
case "", "accept":
|
||||
return "accept"
|
||||
case "tempfail":
|
||||
return "tempfail"
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// saslRealm chooses the realm new SASL accounts live under. It mirrors the
|
||||
// hostname Postfix's SASL layer uses so a client authenticating with a bare
|
||||
// login resolves to the right account.
|
||||
|
||||
@@ -74,7 +74,7 @@ func restoreFromOwnBackup(t *testing.T, password string) restored {
|
||||
func TestPanelBootsOnADataDirectoryRestoredFromItsOwnBackup(t *testing.T) {
|
||||
r := restoreFromOwnBackup(t, "")
|
||||
|
||||
body := getPage(t, r.panel, "/deliveries", signIn(t, r.panel))
|
||||
body := getPage(t, r.panel, "/outbound/log", signIn(t, r.panel))
|
||||
for _, want := range []string{restoreDomain, restoreSubject} {
|
||||
if !strings.Contains(body, want) {
|
||||
t.Errorf("the restored panel's send log does not show %q:\n%s", want, body)
|
||||
@@ -128,7 +128,7 @@ func TestARestoreDoesNotReopenTheSetupLink(t *testing.T) {
|
||||
func TestARestoredPanelHonoursSessionsFromTheArchive(t *testing.T) {
|
||||
r := restoreFromOwnBackup(t, "")
|
||||
|
||||
rec := request(t, r.panel, http.MethodGet, "/deliveries", nil, r.session)
|
||||
rec := request(t, r.panel, http.MethodGet, "/outbound/log", nil, r.session)
|
||||
if rec.Code != http.StatusOK {
|
||||
t.Errorf("a session from before the backup = %d on the restored panel, want 200", rec.Code)
|
||||
}
|
||||
@@ -141,7 +141,7 @@ func TestARestoredPanelHonoursSessionsFromTheArchive(t *testing.T) {
|
||||
func TestAnEncryptedBackupRestoresTheSameWay(t *testing.T) {
|
||||
r := restoreFromOwnBackup(t, "a-long-enough-password")
|
||||
|
||||
body := getPage(t, r.panel, "/deliveries", signIn(t, r.panel))
|
||||
body := getPage(t, r.panel, "/outbound/log", signIn(t, r.panel))
|
||||
if !strings.Contains(body, restoreSubject) {
|
||||
t.Errorf("the panel restored from an encrypted backup lost the send log:\n%s", body)
|
||||
}
|
||||
@@ -384,7 +384,7 @@ func downloadBackup(t *testing.T, h http.Handler, session *http.Cookie, password
|
||||
form.Set("password", password)
|
||||
form.Set("password_confirm", password)
|
||||
}
|
||||
rec := request(t, h, http.MethodPost, "/backup", strings.NewReader(form.Encode()), session)
|
||||
rec := request(t, h, http.MethodPost, "/server/backup", strings.NewReader(form.Encode()), session)
|
||||
if rec.Code != http.StatusOK {
|
||||
t.Fatalf("download a backup = %d, want 200:\n%s", rec.Code, rec.Body.String())
|
||||
}
|
||||
|
||||
@@ -21,7 +21,7 @@
|
||||
|
||||
services:
|
||||
selfpost:
|
||||
image: ghcr.io/mixeme/selfpost:1.9.1
|
||||
image: ghcr.io/mixeme/selfpost:2.0.0
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
SELFPOST_HOSTNAME: "${SELFPOST_HOSTNAME:?set the mail/panel hostname, e.g. mail.example.com}"
|
||||
|
||||
@@ -114,10 +114,10 @@ One process, four roles:
|
||||
parameters (`queue_run_delay`, `minimal_backoff_time`,
|
||||
`maximal_backoff_time`, `maximal_queue_lifetime`, `bounce_queue_lifetime`,
|
||||
`delay_warning_time`) and caches the snapshot on the handlers config. The
|
||||
Mail queue card and a delivery's `deferred` / `bounced` history print those
|
||||
Outbound queue box and a delivery's `deferred` / `bounced` history print those
|
||||
numbers; they never call `postconf` per request. If `postconf` is missing,
|
||||
the panel logs a warning and uses Postfix 3.x compiled-in defaults
|
||||
(`300s` / `4000s` / `5d` / `0`) with a muted note on the card.
|
||||
(`300s` / `4000s` / `5d` / `0`) with a muted note on the box.
|
||||
2. **journal-milter** — unix socket `JOURNAL_MILTER_SOCKET`; records From/To/
|
||||
Subject/SASL user at DATA; enforces level-2 rate limits; **fail-open**
|
||||
(`default_action=accept`) so milter failure does not stop mail. Domain
|
||||
@@ -133,8 +133,8 @@ One process, four roles:
|
||||
3. **log-tailer** — follows `MAIL_LOG`, updates send-log delivery status by
|
||||
queue-id. Send-log `queued → sent` transitions depend on this goroutine alone
|
||||
(`UpdateStatus` is only called from [internal/logtail](../internal/logtail/logtail.go)).
|
||||
4. **rate-limit recalc** — every six hours (and on demand from the domain
|
||||
page), recomputes level-2 **Auto** rate limits from send-log statistics.
|
||||
4. **rate-limit recalc** — every six hours (and on demand from Domain settings and
|
||||
the application form), recomputes level-2 **Auto** rate limits from send-log statistics.
|
||||
|
||||
Milter chain in Postfix: OpenDKIM (tempfail) then journal (accept on failure).
|
||||
|
||||
@@ -159,7 +159,7 @@ panel user cannot read). `follow()` drains the old inode once more before
|
||||
switching descriptors; the panel treats a missing log file as an empty tail, not
|
||||
an error.
|
||||
|
||||
**Read offset is persisted** (`logtail_state` table, migration `0003`): the
|
||||
**Read offset is persisted** (`logtail_state` table, in the baseline schema): the
|
||||
tailer stores its position plus a fingerprint of the log's first 512 bytes, and
|
||||
on start resumes from it, parsing the tail written while the panel was down. If
|
||||
the fingerprint no longer matches (rotated or recreated in the meantime) it reads
|
||||
@@ -185,8 +185,8 @@ rotated past fourteen files while the panel was down, or deleted.
|
||||
|
||||
**Two one-shot reads** sit beside the follow loop and are unrelated to it, both
|
||||
serving panel pages on request: `TailLines` (the last *n* lines, for
|
||||
`/system-log`) and `QueueLines` (the lines carrying one queue-id, for
|
||||
`/deliveries/{id}`). `QueueLines` scans a bounded tail of the current file —
|
||||
`/server/log`) and `QueueLines` (the lines carrying one queue-id, for
|
||||
`/outbound/log/{id}`). `QueueLines` scans a bounded tail of the current file —
|
||||
finding a message's lines means reading rather than seeking — and matches the id
|
||||
anchored on the character before it, since queue ids are hexadecimal runs and a
|
||||
shorter one is regularly the tail of a longer one. Send-log rows outlive the log
|
||||
@@ -197,61 +197,227 @@ state for an older message and the page reports it as such, not as a failure.
|
||||
|
||||
## Panel HTTP surface
|
||||
|
||||
Canonical routes: [internal/web/web.go](../internal/web/web.go). Authenticated
|
||||
unless noted. Routes marked **global** return **404** for domain administrators
|
||||
(`requireGlobal()` in
|
||||
[internal/web/handlers/authz.go](../internal/web/handlers/authz.go)). The table
|
||||
below is a summary — HTMX fragment endpoints
|
||||
(`/status/fragment`, `/deliveries/rows`, `/mail-queue/body`,
|
||||
`/system-log/body`, …) and every POST variant live in `web.go`.
|
||||
Canonical routes: [internal/web/web.go](../internal/web/web.go). The router is
|
||||
two flat `http.ServeMux`es built by `muxes()` — one pattern per route, method
|
||||
included (`GET /outbound/log/{id}`), nothing mounted as a sub-router, and
|
||||
building them only takes method values, so the route guard test can run it on
|
||||
a bare `Server` and ask which pattern answers a path:
|
||||
|
||||
| Route | Purpose |
|
||||
- **public** — `/healthz` (liveness), `/license` (the embedded `LICENSE`),
|
||||
`/static/`, `/setup/`, `/login`, `/logout`;
|
||||
- **authenticated** — everything else, behind `RequireAuth`: a request without
|
||||
a live session is redirected to `/login`.
|
||||
|
||||
Both sit inside `secure()`, which sets the security headers and runs the origin
|
||||
check on every state-changing request (see below).
|
||||
|
||||
### Routes
|
||||
|
||||
A path reads like the menu: `/<group>/<page>`, an entity under its list, an
|
||||
action under the thing it changes. Every state change is a `POST`; a `GET`
|
||||
only reads (the delete confirmation at `GET …/delete` renders a form).
|
||||
|
||||
| Group | Routes |
|
||||
|---|---|
|
||||
| `/healthz` | Liveness (no auth) |
|
||||
| `/license` | Embedded `LICENSE` text (no auth) |
|
||||
| `/setup/*` | One-time admin bootstrap |
|
||||
| `/login`, `/logout` | Session auth |
|
||||
| `/account` | 308 redirect to `/settings` (pre-1.2.3 route, kept as a compat shim) |
|
||||
| `/status`, `/status/*` | **Global.** Process, cert, socket, PTR checks; machine CPU/memory/network |
|
||||
| `/domains` | Domain list; `POST /domains` (add domain) is **global** |
|
||||
| `/domains/{id}`, `/domains/{id}/*` | Assigned-domain detail for domain-admins; delete domain is **global** |
|
||||
| `/domains/import` | **Global.** Domain import (`POST`; form on the Backup page) |
|
||||
| `/deliveries`, `/deliveries/{id}` | Send log with filters; scoped to assigned domains for domain-admins |
|
||||
| `/mail-queue`, `/mail-queue/*` | **Global.** Postfix queue view; retry-policy card on the page (not the HTMX fragment) |
|
||||
| `/system-log`, `/system-log/*` | **Global.** `mail.log` tail |
|
||||
| `/reload` | **Global.** `POST` — reload OpenDKIM tables, Postfix sender map, and inbound relay maps when enabled (not DMARC maps — see § Persistence restore) |
|
||||
| `/backup`, `/backup/*` | **Global.** Full backup download (page also hosts the import form) |
|
||||
| `/help` | In-panel operator help (any authenticated user) |
|
||||
| `/settings` | Username/password for any user; DMARC report default is **global** only |
|
||||
| `/users`, `/users/*` | **Global.** Panel user CRUD |
|
||||
| `/inbound`, `/inbound/{id}`, `/inbound/{id}/*` | **Global.** Inbound relay domains. Registered only when `INBOUND_RELAY_ENABLE=true`; otherwise 404. |
|
||||
| `/dmarc`, `/dmarc/reports/{id}`, `/dmarc/domains/{id}` | **Global** list and report detail; domain roll-up scoped like deliveries. Registered only when `DMARC_REPORTS_ENABLE=true`; otherwise 404. |
|
||||
| Home | `GET /` → `/overview`, or for a user with the *domain* role to the first list they reach (`/outbound/domains`, or `/inbound/domains` when that is all) |
|
||||
| Overview | `GET /overview`, `GET /overview/fragment` |
|
||||
| Outbound › Domains | `GET, POST /outbound/domains` · `/outbound/domains/{id}` · `POST …/dns-recheck` · `GET, POST …/delete` · `GET …/settings` · `POST …/settings/reports`, `…/ratelimit`, `…/ratelimit/recalc`, `…/export` |
|
||||
| Outbound › Applications | `GET, POST /outbound/domains/{id}/applications/new` · `GET, POST …/applications/{aid}` · `POST …/applications/{aid}/password`, `…/{aid}/ratelimit/recalc`, `…/{aid}/delete` |
|
||||
| Outbound › Log, Queue | `GET /outbound/log` · `/outbound/log/fragment` · `/outbound/log/{id}` · `GET /outbound/queue` · `/outbound/queue/fragment` |
|
||||
| Outbound › DMARC reports | `GET /outbound/dmarc` · `…/dmarc/domains/{id}` · `…/dmarc/reports/{id}` — registered only with `DMARC_REPORTS_ENABLE=true` |
|
||||
| Inbound › Domains | `GET, POST /inbound/domains` · `/inbound/domains/{id}` · `POST …/dns-recheck`, `…/upstream`, `…/recipients` · `GET, POST …/delete` — registered only with `INBOUND_RELAY_ENABLE=true` |
|
||||
| Server | `GET /server/health` (+ `/fragment`) · `POST /server/health/recheck`, `/server/health/reload` · `GET /server/log` (+ `/fragment`) · `GET, POST /server/backup` · `POST /server/backup/import` · `/server/users`, `…/new`, `…/{uid}`, `…/{uid}/delete` · `GET, POST /server/settings` · `GET /server/components` |
|
||||
| Account | `GET /account` · `POST /account/profile`, `/account/password` |
|
||||
| User menu | `GET /help` · `POST /logout` (public mux) |
|
||||
|
||||
HTMX polling refreshes monitoring fragments (5 s while the operator is active on
|
||||
the page, 30 s when the tab is visible but idle, none when hidden — scheduled in
|
||||
`panel.js` via `data-poll`, not `hx-trigger="every …"`); polling does not extend
|
||||
session idle timeout (only non-`HX-Request` GET and mutating requests count as
|
||||
activity). The Mail queue retry-policy card is outside that fragment: it is the
|
||||
start-up `postconf -h` snapshot (see [Panel binary](#panel-binary-cmdpanel)),
|
||||
not a live re-read.
|
||||
An application is addressed under its domain, and an application id that does
|
||||
not belong to the domain in the path is a 404. The application form is one
|
||||
`POST`: sender, client IPs and rate limit are validated and saved together or
|
||||
not at all, also at creation. The page that shows a new password is the
|
||||
response to the `POST` that created or regenerated it; it has no `GET` path and
|
||||
carries `Cache-Control: no-store`.
|
||||
|
||||
The pre-2.0 paths are not served and not redirected (`/status`, `/domains/…`,
|
||||
`/inbound/{id}/…`, `/deliveries`, `/mail-queue`, `/system-log`, `/backup`,
|
||||
`/users/…`, `/settings`, `/reload`, `/dmarc/…`, and `/account` as a redirect):
|
||||
each answers 404 once signed in. `TestRoutesFollowNavigation`
|
||||
([internal/web/guard_routes_test.go](../internal/web/guard_routes_test.go))
|
||||
holds the table: every path is registered with its methods, every menu entry
|
||||
points at a registered path, every old path answers 404, and no template
|
||||
contains an old path.
|
||||
|
||||
### Who may open what
|
||||
|
||||
Authorization is by tree where the tree is the boundary, and by object where it
|
||||
is not.
|
||||
|
||||
- **`/server/` is guarded once.** Routes under it are registered through
|
||||
`server()` in `web.go`, which wraps the handler in `globalOnly` (404 for
|
||||
anyone but the global role — the answer a missing page gets, so the panel does
|
||||
not confirm the page exists) and panics at start-up for a pattern outside
|
||||
`/server/`. The handlers also call `requireGlobal()` themselves.
|
||||
- **Overview and the Outbound queue** are global-only but not under `/server/`,
|
||||
so their handlers call `requireGlobal()`.
|
||||
- **Outbound is checked per domain.** `lookupDomain` resolves `{id}` and answers
|
||||
404 for a missing domain and for one the principal may not reach alike
|
||||
([handlers/handlers_domains.go](../internal/web/handlers/handlers_domains.go));
|
||||
application routes go on to check the application belongs to that domain.
|
||||
Lists and the log are filtered by assignment in the query
|
||||
(`ListDomainsForUser`), not in the template. Adding and deleting a domain is
|
||||
`requireGlobal()`.
|
||||
- **Inbound is checked per inbound domain**, separately from outbound
|
||||
([handlers/handlers_inbound.go](../internal/web/handlers/handlers_inbound.go)):
|
||||
`requireInbound` (the list), `requireInboundDomain` (one domain — page,
|
||||
upstream, recipients, DNS re-check) and `requireInboundGlobal` (add, delete).
|
||||
All three answer 404 alike.
|
||||
- **The DMARC hub** is open to anyone with outbound reach for the reports of
|
||||
their own domains; the *Ingest* box (server-wide statistics) is rendered for
|
||||
the global role only.
|
||||
- **Account and Help** are open to every signed-in user.
|
||||
- The principal ([web/auth/principal.go](../internal/web/auth/principal.go))
|
||||
carries the role, the assigned outbound and inbound domain ids and the two
|
||||
*All* flags; `CanAccessDomain` and `CanAccessInboundDomain` are the only
|
||||
predicates. The reach, not the role, decides which menu groups exist.
|
||||
|
||||
Routes marked global are tested per path for a user with the *domain* role,
|
||||
including another tenant's domain id
|
||||
([handlers/handlers_delegation_test.go](../internal/web/handlers/handlers_delegation_test.go),
|
||||
[authz_test.go](../internal/web/handlers/authz_test.go)).
|
||||
|
||||
### Polled fragments
|
||||
|
||||
A polled page answers its refresh at `<page>/fragment` (`/overview/fragment`,
|
||||
`/outbound/log/fragment`, `/server/health/fragment`, …). The fragment is the
|
||||
same `{{define}}` block that renders the page's own box, so the first paint and
|
||||
a refresh cannot differ. HTMX refreshes them every 5 s while the operator is
|
||||
active on the page, every 30 s when the tab is visible but idle, and not at all
|
||||
when it is hidden — scheduled in `panel.js` via `data-poll`, not
|
||||
`hx-trigger="every …"`, because a trigger filter is evaluated with `new
|
||||
Function`, which the CSP forbids. Polling does not extend the session idle
|
||||
timeout (only non-`HX-Request` `GET`s and mutating requests count as
|
||||
activity). The Outbound queue's retry-policy box is outside the polled region:
|
||||
it is the start-up `postconf -h` snapshot (see [Panel
|
||||
binary](#panel-binary-cmdpanel)), not a live re-read.
|
||||
|
||||
### View engine and component kit
|
||||
|
||||
Pages are rendered by [internal/web/view](../internal/web/view/view.go). There
|
||||
is one layout and one way to render.
|
||||
|
||||
- **One layout.** Every page is parsed with `layout.html` (the shell: brick
|
||||
navbar with the wordmark, the perforated edge, the strip of sibling pages,
|
||||
user menu, footer) and `components.html` (the partials), then its own file.
|
||||
`layout_signed_out` is the same document without navigation, used for sign-in
|
||||
and setup; `Engine.Render` picks it when the page data names no user. The
|
||||
page's file defines `content`; `pageFiles` in `view.go` lists the files of
|
||||
each page, and fragments are listed in `fragmentFiles` and rendered by
|
||||
`RenderFragment` with no layout.
|
||||
- **Typed page data.** A page's data is a struct that embeds `Meta` (title,
|
||||
user, the viewer's reach, the menu group and page it sits in) and carries
|
||||
the page's boxes and rows as fields, built by a constructor in the `view`
|
||||
package (`NewOutDomain`, `NewHelp`, …) that the handler fills. The shell is
|
||||
derived from `Meta` and the feature flags (`SetInboundEnabled`,
|
||||
`SetDMARCEnabled`) in one place, `Engine.shell`, so which entries the menu
|
||||
has is decided once. No template reads a `map[string]any`; the one map left
|
||||
in the handlers is the health sampler's, which is turned into a typed
|
||||
`Health` before rendering.
|
||||
- **The component kit.** [components.html](../internal/web/view/templates/components.html)
|
||||
holds one `{{define}}` per partial (page head, postmark, box and its head and
|
||||
foot, flash, health card, DNS record, facts, side menu, timeline, log pane,
|
||||
empty state, confirm list, help topic, form rows) and is the only file that
|
||||
writes their `sp-` markup. [components.go](../internal/web/view/components.go)
|
||||
is the typed input of each (`Head`, `Box`, `Record`, `Fact`, `SideMenu`, …),
|
||||
so a page passes a struct, never a `dict`. Template functions: `status_tag`
|
||||
(a status → Bulma's tag classes, once), `wbr_at` (a break after the `@` in a
|
||||
table cell). A page that needs a component the kit lacks is a design change
|
||||
made in the mockups first
|
||||
([plans/panel-redesign.md](plans/panel-redesign.md) § The contract).
|
||||
- **Help.** [help.go](../internal/web/view/help.go) is the Help page's data:
|
||||
topics grouped by section of the panel. A box's head links to its topic with
|
||||
`Box.Help` (`/help#<topic>`), and the page lists only the
|
||||
sections the viewer's menu shows.
|
||||
- **Assets.** [static/](../internal/web/view/static) is embedded
|
||||
(`//go:embed`): vendored `bulma.min.css` and a Tabler icon subset (checksummed),
|
||||
`panel.css` (the mockups' stylesheet, rule for rule), `htmx.min.js`, `panel.js`,
|
||||
the IBM Plex subset and the logo files. Served with a content-derived `ETag`,
|
||||
and `/static/` does not list its files. The kit page
|
||||
`GET /server/components` renders every partial in every state from fixtures
|
||||
(global role only).
|
||||
|
||||
**Guard tests** hold the design in place; they run in `go test ./...` and CI.
|
||||
They read their rules from the accepted mockups
|
||||
([docs/assets/panel-redesign/panel/](assets/panel-redesign/index.html)) rather
|
||||
than keep a copy: `TestPanelClassVocabulary` (only the Bulma subset and the
|
||||
`sp-` classes of `panel.css`; every `sp-` class defined, used and shown on the
|
||||
components page), `TestPanelCSSIsTheMockupStylesheet`, `TestPanelCSSContract`
|
||||
(no `!important`, no id or page selector, type floor, widths),
|
||||
`TestTemplatesUseComponents` (no hand-written component markup in a page),
|
||||
`TestPanelOutlinesMatchMockups` and `TestPanelPageStructure` (each rendered page
|
||||
has the component skeleton of its mockup), `TestRedesignedPagesLoadOnlyTheKit`,
|
||||
`TestVendoredAssetsArePinned`, `TestStaticHoldsOnlyTheKnownAssets`,
|
||||
`TestRoutesFollowNavigation`, `TestNoTemplateUsesInlineScriptOrStyle`. The
|
||||
ratchet list `internal/web/view/legacy_pages.txt` is empty and can only shrink;
|
||||
the CI step
|
||||
[design-first](../.github/scripts/design-first.sh) rejects a commit that
|
||||
changes a guard together with the code it judges, and a template that is a
|
||||
byte-for-byte return of one from history. The design contract and the process
|
||||
are in [plans/panel-redesign.md](plans/panel-redesign.md) § Enforcement.
|
||||
|
||||
### Security headers, CSP and scripts
|
||||
|
||||
`secure()` ([internal/web/security.go](../internal/web/security.go)) puts these
|
||||
on every response: `Content-Security-Policy: default-src 'self'; object-src
|
||||
'none'; base-uri 'none'; form-action 'self'; frame-ancestors 'none'`,
|
||||
`X-Content-Type-Options: nosniff`, `X-Frame-Options: DENY`,
|
||||
`Referrer-Policy: no-referrer`, and `Strict-Transport-Security` when the cookie
|
||||
is `Secure`. Because the policy has no exemptions, no template carries an
|
||||
inline `<script>`, an event-handler attribute or `style="…"` — the page's
|
||||
behaviour is `htmx.min.js` plus `panel.js` (copy and *fill* buttons, the
|
||||
`data-confirm` prompt on destructive forms, conditional field visibility,
|
||||
adaptive polling), both optional: every form works as a native HTML `POST`
|
||||
with scripts off. htmx is told not to inject its indicator stylesheet
|
||||
(`htmx-config` meta), which the CSP would otherwise have to allow.
|
||||
|
||||
State-changing requests (every non-`GET`/`HEAD`/`OPTIONS`) pass `originAllowed`
|
||||
first: `Sec-Fetch-Site` must say `same-origin`, or `Origin` must name the
|
||||
panel's host; a request with neither header is let through (an accepted risk).
|
||||
There are no CSRF tokens — see [security.md](security.md).
|
||||
|
||||
### Errors shown to users
|
||||
|
||||
A validation refusal is shown as the text the validator wrote (on the form,
|
||||
next to the field, or in a notice above the first box). Anything else — a
|
||||
database error, `saslpasswd2`, a Postfix reload — is logged
|
||||
and the page gets a fixed sentence saying so; the error text never reaches the
|
||||
browser. A template error renders a bare `500` rather than a half-written page
|
||||
(`Engine.Render` buffers).
|
||||
|
||||
### Sessions
|
||||
|
||||
Stored in SQLite (`sessions` table, migration `0002`): cookie holds a random
|
||||
token; the database stores **SHA-256 of the token**, not the token itself — a
|
||||
stolen DB or backup archive does not alone grant login, but a browser that still
|
||||
holds the cookie works after process restart, redeploy, or full backup restore.
|
||||
Stored in SQLite (`sessions` table of the baseline, `0001_init.sql`): the cookie
|
||||
holds a random token; the database stores **SHA-256 of the token**, not the
|
||||
token itself — a stolen DB or backup archive does not alone grant login, but a
|
||||
browser that still holds the cookie works after process restart, redeploy, or
|
||||
full backup restore.
|
||||
|
||||
- **A session belongs to a user by id** (`sessions.user_id`, `ON DELETE
|
||||
CASCADE`). Deleting a user ends their sessions; renaming one keeps them;
|
||||
`RequireAuth` loads the user on every request, so a session whose user is
|
||||
gone is a redirect to `/login`.
|
||||
- **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/auth/session.go](../internal/web/auth/session.go)).
|
||||
- **Password change on `/settings`** — changing your own password deletes
|
||||
every other session for that user; the current session stays active
|
||||
- **Renewal** — the DB `expires_at` and the cookie `Max-Age` update at most once per
|
||||
hour (`renewThreshold` in
|
||||
[internal/web/auth/session.go](../internal/web/auth/session.go)).
|
||||
- **Password change on Account** — deletes every other session of that user;
|
||||
the current one stays active
|
||||
([internal/store/sessions.go](../internal/store/sessions.go),
|
||||
[handlers_settings.go](../internal/web/handlers/handlers_settings.go)).
|
||||
A global administrator resetting another user's password on `/users` updates
|
||||
the hash but does not delete that user's existing sessions.
|
||||
[handlers_account.go](../internal/web/handlers/handlers_account.go)).
|
||||
- **A password set by a global user on the user form** deletes all of that
|
||||
user's sessions (`DestroyUserSessions`); set for oneself, it keeps the
|
||||
current one
|
||||
([handlers_users.go](../internal/web/handlers/handlers_users.go),
|
||||
`endSessionsAfterPasswordSet`).
|
||||
|
||||
Restoring an **older** backup also restores session rows: a session removed
|
||||
after that backup was taken can become valid again if the browser still holds
|
||||
@@ -264,7 +430,7 @@ the cookie and the restored row's `expires_at` has not passed.
|
||||
Multi-store writes that must land in more than one place (SQLite row,
|
||||
`sasldb2` entry, Postfix map, OpenDKIM table) go through a service, which is
|
||||
also where the rollback of a partial failure lives. Handlers may call
|
||||
`store` directly for single-table reads and simple writes (sessions, admin,
|
||||
`store` directly for single-table reads and simple writes (sessions, users,
|
||||
send-log queries); the first-run setup-token file is read and written in
|
||||
`web` itself. The adapters below the services are the only code that knows
|
||||
about Postfix, OpenDKIM, DNS or the log file, which is what makes them
|
||||
@@ -295,7 +461,7 @@ flowchart TB
|
||||
dmarcSvc["internal/dmarc"]
|
||||
end
|
||||
subgraph persistence ["Persistence"]
|
||||
store["internal/store — SQLite, embedded migrations"]
|
||||
store["internal/store — SQLite, embedded schema"]
|
||||
end
|
||||
subgraph adapters ["Adapters — the only infrastructure-aware code"]
|
||||
postfix["internal/postfix"]
|
||||
@@ -344,7 +510,7 @@ single-connection trade-off that follows from it.
|
||||
|
||||
| Path | Contents |
|
||||
|---|---|
|
||||
| `selfpost.db` | SQLite panel state (domains, apps, users, sessions, send log, L2 limits, log-tailer offset, inbound relay, DMARC reports) — see [schema-migrations.md](schema-migrations.md) |
|
||||
| `selfpost.db` | SQLite panel state (users, sessions, domains, apps, send log, L2 limits, log-tailer offset, inbound relay, DMARC reports) — see [Database](#database) and [schema-migrations.md](schema-migrations.md) |
|
||||
| `setup-token` | First-run setup token file |
|
||||
| `opendkim/` | DKIM keys + tables |
|
||||
| `sasl/sasldb2` | Application SASL credentials |
|
||||
@@ -363,6 +529,49 @@ single-connection trade-off that follows from it.
|
||||
Not in `/data`: TLS certificates for the panel (reverse-proxy mount) — though
|
||||
full backups also archive the operator's `./certs` PEM files when present.
|
||||
|
||||
### Database
|
||||
|
||||
The schema is one embedded baseline, `internal/store/migrations/0001_init.sql`,
|
||||
applied by `store.Open` into an empty file; `PRAGMA user_version` is `1` and
|
||||
`PRAGMA application_id` is `0x53503230` ("SP20"). There is no 1.x migration
|
||||
chain. **2.0 starts from an empty data directory:** a database with a schema
|
||||
version but without the 2.x `application_id` — what every 1.x file looks like —
|
||||
is refused at start with `ErrForeignSchema` and left untouched, as is one whose
|
||||
version is newer than the build knows. A restored 1.x backup fails the same
|
||||
way. Domains are carried across with a domain export, whose format is not the
|
||||
database. Later schema changes are new files (`0002_*.sql`, …); once `2.0.0` is
|
||||
released the baseline is never edited. History and the per-table list:
|
||||
[schema-migrations.md](schema-migrations.md).
|
||||
|
||||
The tables that carry the panel's model:
|
||||
|
||||
- **`users`** — `role` is `global` or `domain` (a reach, not a rank); `email` is
|
||||
the user's own address, edited on Account and set by the global role on the
|
||||
user form; `all_domains` / `all_inbound_domains` widen a *domain* user to
|
||||
every domain of that direction, including ones added later (with a flag set
|
||||
the assignment rows are ignored).
|
||||
- **`user_domains`** and **`user_inbound_domains`** — the outbound and inbound
|
||||
domains assigned to a *domain* user. Separate tables: the same name on both
|
||||
is two assignments, and a sending domain gives no inbound access or the other
|
||||
way round.
|
||||
- **`sessions`** — `token_hash`, `user_id` (`ON DELETE CASCADE`), `expires_at`.
|
||||
Keyed by user id, not by name.
|
||||
- **`domains`** — `dmarc_rua` is the whole of a domain's report-address choice:
|
||||
`''` is no reports (a new domain), the address SelfPost derives for the
|
||||
domain and the server's hostname is the hosted choice, anything else is an
|
||||
address typed for the domain. Nothing refers to a user, and deleting one
|
||||
changes no domain.
|
||||
- **`settings`** — key/value, what holds for the instance whoever is signed in
|
||||
(send-log retention). Nothing that belongs to a user.
|
||||
- `applications` (with `auth_ip_restrict` / `auth_allowed_ips`),
|
||||
`application_addresses`, `rate_limits` (no `allowed_ips` column), `send_log`,
|
||||
`logtail_state`, `inbound_domains`, `inbound_transports`,
|
||||
`inbound_recipients`, `dmarc_reports`, `dmarc_report_records`.
|
||||
|
||||
The panel is the only writer; SQLite runs in WAL mode on one connection (see
|
||||
[Panel binary](#panel-binary-cmdpanel)), which is why a full backup snapshots
|
||||
with `VACUUM INTO` instead of copying the file.
|
||||
|
||||
**Rotation:** send-log retention `SEND_LOG_RETENTION_DAYS` (default 90);
|
||||
`mail.log` via logrotate (14 rotated files, check every 6h, rename +
|
||||
`postfix reload` in `postrotate` — see § Log tailer above).
|
||||
@@ -377,7 +586,8 @@ tables, Postfix's sender map, inbound relay maps (when
|
||||
`INBOUND_RELAY_ENABLE=true`), and DMARC maps (when `DMARC_REPORTS_ENABLE=true`)
|
||||
are re-derived from SQLite and both daemons are reloaded, so drift between the
|
||||
extracted archive and the database is healed before mail flows. Manual
|
||||
`POST /reload` on the Status page resyncs OpenDKIM, the sender map, and
|
||||
`POST /server/health/reload` (the *Reload configuration* button on Server ›
|
||||
Health) resyncs OpenDKIM, the sender map, and
|
||||
inbound maps only — not DMARC maps. Stopped-container
|
||||
`tar` of `./data` alone remains possible for state-only copies (see guide).
|
||||
|
||||
@@ -386,9 +596,12 @@ inbound maps only — not DMARC maps. Stopped-container
|
||||
scrypt → AES-256-GCM over 64 KiB chunks, each authenticated with the header,
|
||||
its counter and an end-of-stream flag (so truncation and reordering fail to
|
||||
open). Full backup `.tar.gz` → `.spbk` (SelfPost backup), domain export
|
||||
`.json` → `.spde` (SelfPost domain export); the plain forms remain the
|
||||
default. Domain import detects the envelope by magic bytes; an encrypted full
|
||||
backup is converted back with `selfpost-backup -decrypt` before restore.
|
||||
`.json` → `.spde` (SelfPost domain export). In the panel the *Encrypt with a
|
||||
password* box is ticked by default on both downloads and the plain form is
|
||||
what an unticked box gives; the `selfpost-backup` CLI stays plain unless it is
|
||||
given a password. Domain import detects the envelope by magic bytes; an
|
||||
encrypted full backup is converted back with `selfpost-backup -decrypt` before
|
||||
restore.
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -0,0 +1,164 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en" data-theme="light">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>example.org · Outbound domains · SelfPost</title>
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@picocss/pico@2.1.1/css/pico.min.css">
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@tabler/icons-webfont@3/dist/tabler-icons.min.css">
|
||||
<link rel="stylesheet" href="../shared/brand.css">
|
||||
<link rel="stylesheet" href="theme.css">
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<header class="sp-bar">
|
||||
<div class="sp-wrap">
|
||||
<nav>
|
||||
<ul>
|
||||
<li><a class="sp-brand" href="status.html"><img src="../../selfpost-stamp-compact.svg" alt="SelfPost"></a></li>
|
||||
</ul>
|
||||
<ul class="sp-sections">
|
||||
<li><a href="status.html">Overview</a></li>
|
||||
<li><a href="domain.html" aria-current="true">Outbound</a></li>
|
||||
<li><a href="#">Inbound</a></li>
|
||||
<li><a href="#">Server</a></li>
|
||||
</ul>
|
||||
<ul class="sp-user">
|
||||
<li>
|
||||
<details class="dropdown">
|
||||
<summary>admin</summary>
|
||||
<ul dir="rtl">
|
||||
<li><a href="#">Account</a></li>
|
||||
<li><a href="#">Help</a></li>
|
||||
<li><a href="login.html">Sign out</a></li>
|
||||
</ul>
|
||||
</details>
|
||||
</li>
|
||||
</ul>
|
||||
</nav>
|
||||
</div>
|
||||
</header>
|
||||
<div class="sp-pages">
|
||||
<div class="sp-wrap">
|
||||
<a href="#" aria-current="true">Domains</a>
|
||||
<a href="#">Log</a>
|
||||
<a href="#">Queue</a>
|
||||
<a href="#">DMARC reports</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<main class="sp-wrap">
|
||||
<div class="sp-head">
|
||||
<div>
|
||||
<p class="sp-kicker"><a href="#">Outbound domains</a> /</p>
|
||||
<h1>example.org</h1>
|
||||
</div>
|
||||
<small>1 284 messages in 30 days · peak 96 msg/h · average 1.8 msg/h</small>
|
||||
</div>
|
||||
|
||||
<section class="sp-sheet">
|
||||
<header><span class="sp-no">01</span><h2>Connection</h2><small>The same for every domain</small></header>
|
||||
<div class="sp-body sp-cols">
|
||||
<dl class="sp-kv">
|
||||
<dt>Server</dt>
|
||||
<dd><span class="sp-value">mail.example.org</span><button type="button" class="outline sp-copy">Copy</button></dd>
|
||||
</dl>
|
||||
<dl class="sp-kv">
|
||||
<dt>Ports</dt>
|
||||
<dd><span class="sp-value">465 — SSL/TLS (implicit)<br>587 — STARTTLS (submission)</span></dd>
|
||||
</dl>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="sp-sheet">
|
||||
<header>
|
||||
<span class="sp-no">02</span><h2>DNS records</h2>
|
||||
<small>Checked 3 min ago</small>
|
||||
<form method="post" action="#"><button type="submit" class="outline secondary sp-btn-sm">Re-check</button></form>
|
||||
</header>
|
||||
|
||||
<div class="sp-record">
|
||||
<div class="sp-record-head"><h3>DKIM</h3><span class="sp-tag ok">published</span><small>Not a secret · selector <code>sp2026</code></small></div>
|
||||
<dl class="sp-kv">
|
||||
<dt>Host</dt><dd><span class="sp-value">sp2026._domainkey.example.org</span><button type="button" class="outline sp-copy">Copy</button></dd>
|
||||
<dt>Type</dt><dd><span class="sp-value plain">TXT</span></dd>
|
||||
<dt>Value</dt><dd><span class="sp-value">v=DKIM1; k=rsa; p=MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAx3kqV0mJd8e2f1Yb7Qw5uT9sLr4nHc6pZaXo1vKe0yWm2gBt8RjN5dCq3hUf7lPs9aEi4oMz6xTb1kGv0nYw…IDAQAB</span><button type="button" class="outline sp-copy">Copy</button></dd>
|
||||
</dl>
|
||||
</div>
|
||||
|
||||
<div class="sp-record">
|
||||
<div class="sp-record-head"><h3>SPF</h3><span class="sp-tag fail">mismatch</span><small>Merge into the existing record — never publish a second one</small></div>
|
||||
<dl class="sp-kv">
|
||||
<dt>Host</dt><dd><span class="sp-value">example.org</span><button type="button" class="outline sp-copy">Copy</button></dd>
|
||||
<dt>Type</dt><dd><span class="sp-value plain">TXT</span></dd>
|
||||
<dt>Value</dt><dd><span class="sp-value">v=spf1 ip4:203.0.113.25 ~all</span><button type="button" class="outline sp-copy">Copy</button></dd>
|
||||
<dt>In DNS now</dt><dd><span class="sp-value">v=spf1 include:_spf.google.com ~all</span></dd>
|
||||
</dl>
|
||||
<p class="sp-note fail">The published record does not authorize 203.0.113.25. Add <code>ip4:203.0.113.25</code> before <code>~all</code>.</p>
|
||||
</div>
|
||||
|
||||
<div class="sp-record">
|
||||
<div class="sp-record-head"><h3>DMARC</h3><span class="sp-tag ok">published</span><small>Report address from <a href="#">Settings</a></small></div>
|
||||
<dl class="sp-kv">
|
||||
<dt>Host</dt><dd><span class="sp-value">_dmarc.example.org</span><button type="button" class="outline sp-copy">Copy</button></dd>
|
||||
<dt>Type</dt><dd><span class="sp-value plain">TXT</span></dd>
|
||||
<dt>Value</dt><dd><span class="sp-value">v=DMARC1; p=none; rua=mailto:dmarc@mail.example.org</span><button type="button" class="outline sp-copy">Copy</button></dd>
|
||||
</dl>
|
||||
<p class="sp-note"><code>p=none</code> does not affect delivery. Tighten to <code>quarantine</code>, then <code>reject</code>, once the <a href="#">reports</a> look clean.</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="sp-sheet">
|
||||
<header>
|
||||
<span class="sp-no">03</span><h2>Applications</h2>
|
||||
<small>SASL logins that may send as this domain</small>
|
||||
<a href="#" role="button" class="sp-btn-sm">Add application</a>
|
||||
</header>
|
||||
<table class="sp-apps">
|
||||
<thead><tr><th>Login</th><th>May send as</th><th>30 days</th><th>Limits</th><th></th></tr></thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td class="sp-login">prod-server</td>
|
||||
<td>*@example.org</td>
|
||||
<td>1 102 msg · peak 96/h</td>
|
||||
<td><span class="sp-tag ok">200 / h</span></td>
|
||||
<td><a class="sp-link-btn" href="#">Edit</a><a class="sp-link-btn" href="#">New password</a><a class="sp-link-btn danger" href="#">Delete</a></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="sp-login">alerts</td>
|
||||
<td>alerts@example.org, noc@example.org</td>
|
||||
<td>182 msg · peak 14/h</td>
|
||||
<td><span class="sp-tag">domain</span> <span class="sp-tag ok">2 IPs</span></td>
|
||||
<td><a class="sp-link-btn" href="#">Edit</a><a class="sp-link-btn" href="#">New password</a><a class="sp-link-btn danger" href="#">Delete</a></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</section>
|
||||
|
||||
<section class="sp-sheet sp-ledger">
|
||||
<header><span class="sp-no">04</span><h2>Domain settings</h2><small>Rarely changed</small></header>
|
||||
<details>
|
||||
<summary><strong>DMARC reports</strong><span class="sp-muted">Same as Settings — dmarc@mail.example.org</span><span class="sp-tag">default</span></summary>
|
||||
<div class="sp-detail"><p class="sp-muted">The form opens here.</p></div>
|
||||
</details>
|
||||
<details>
|
||||
<summary><strong>Rate limit</strong><span class="sp-muted">Auto ×3 — 288 messages / hour, recalculated yesterday</span><span class="sp-tag ok">active</span></summary>
|
||||
<div class="sp-detail"><p class="sp-muted">The form opens here.</p></div>
|
||||
</details>
|
||||
<details>
|
||||
<summary><strong>Export</strong><span class="sp-muted">DKIM key and applications as one file, optionally encrypted</span><span></span></summary>
|
||||
<div class="sp-detail"><p class="sp-muted">The form opens here.</p></div>
|
||||
</details>
|
||||
<details>
|
||||
<summary><strong>Delete domain</strong><span class="sp-muted">Removes the DKIM key and both applications</span><span></span></summary>
|
||||
<div class="sp-detail"><p class="sp-muted">The confirmation opens here.</p></div>
|
||||
</details>
|
||||
</section>
|
||||
|
||||
<footer class="sp-foot">
|
||||
SelfPost 1.8.0 · © 2026 Mix · <a href="#">License (AGPL-3.0)</a> · <a href="#">Source</a> · No warranty
|
||||
</footer>
|
||||
</main>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,37 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en" data-theme="light">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>Sign in · SelfPost</title>
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@picocss/pico@2.1.1/css/pico.min.css">
|
||||
<link rel="stylesheet" href="../shared/brand.css">
|
||||
<link rel="stylesheet" href="theme.css">
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<div class="sp-signin">
|
||||
<div class="sp-signin-card">
|
||||
<img src="../../selfpost-stamp.svg" alt="SelfPost">
|
||||
<section class="sp-sheet">
|
||||
<header><span class="sp-no">→</span><h2>Sign in</h2></header>
|
||||
<div class="sp-body">
|
||||
<form method="post" action="status.html">
|
||||
<label for="username">Username</label>
|
||||
<input id="username" name="username" autocomplete="username" required>
|
||||
<label for="password">Password</label>
|
||||
<input id="password" name="password" type="password" autocomplete="current-password" required>
|
||||
<button type="submit">Sign in</button>
|
||||
</form>
|
||||
</div>
|
||||
</section>
|
||||
<p class="sp-tagline">YOUR SMTP. YOUR RULES.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<footer class="sp-wrap sp-foot">
|
||||
© 2026 Mix · <a href="#">License (AGPL-3.0)</a> · <a href="#">Source</a> · No warranty
|
||||
</footer>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,136 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en" data-theme="light">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>Overview · SelfPost</title>
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@picocss/pico@2.1.1/css/pico.min.css">
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@tabler/icons-webfont@3/dist/tabler-icons.min.css">
|
||||
<link rel="stylesheet" href="../shared/brand.css">
|
||||
<link rel="stylesheet" href="theme.css">
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<header class="sp-bar">
|
||||
<div class="sp-wrap">
|
||||
<nav>
|
||||
<ul>
|
||||
<li><a class="sp-brand" href="status.html"><img src="../../selfpost-stamp-compact.svg" alt="SelfPost"></a></li>
|
||||
</ul>
|
||||
<ul class="sp-sections">
|
||||
<li><a href="status.html" aria-current="page">Overview</a></li>
|
||||
<li><a href="domain.html">Outbound</a></li>
|
||||
<li><a href="#">Inbound</a></li>
|
||||
<li><a href="#">Server</a></li>
|
||||
</ul>
|
||||
<ul class="sp-user">
|
||||
<li>
|
||||
<details class="dropdown">
|
||||
<summary>admin</summary>
|
||||
<ul dir="rtl">
|
||||
<li><a href="#">Account</a></li>
|
||||
<li><a href="#">Help</a></li>
|
||||
<li><a href="login.html">Sign out</a></li>
|
||||
</ul>
|
||||
</details>
|
||||
</li>
|
||||
</ul>
|
||||
</nav>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<main class="sp-wrap">
|
||||
<div class="sp-head">
|
||||
<div>
|
||||
<p class="sp-kicker">mail.example.org</p>
|
||||
<h1>Overview</h1>
|
||||
</div>
|
||||
<small>Updated 14:02:31 UTC · refreshes every 10 s</small>
|
||||
</div>
|
||||
|
||||
<section class="sp-sheet sp-verdict">
|
||||
<div class="sp-postmark warn"><span>SELFPOST</span><b>WARN</b><span>1 OF 6 CHECKS</span></div>
|
||||
<div>
|
||||
<h2>Mail is flowing. One thing needs attention.</h2>
|
||||
<p>The TLS certificate expires in 12 days and has not renewed yet. Everything else is healthy.</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="sp-sheet sp-ledger">
|
||||
<header><span class="sp-no">01</span><h2>Checks</h2><small>Open a row for its detail</small></header>
|
||||
|
||||
<details>
|
||||
<summary><strong>Processes</strong><span class="sp-muted">5 of 5 running</span><span class="sp-tag ok">ok</span></summary>
|
||||
<div class="sp-detail">
|
||||
<table>
|
||||
<thead><tr><th>Program</th><th>State</th><th>Detail</th></tr></thead>
|
||||
<tbody>
|
||||
<tr><td>postfix</td><td><span class="sp-tag ok">running</span></td><td class="sp-muted">pid 41, uptime 18 days</td></tr>
|
||||
<tr><td>opendkim</td><td><span class="sp-tag ok">running</span></td><td class="sp-muted">pid 39, uptime 18 days</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</details>
|
||||
|
||||
<details open>
|
||||
<summary><strong>TLS certificate</strong><span class="sp-muted">Expires in 12 days — renewal has not run</span><span class="sp-tag warn">warn</span></summary>
|
||||
<div class="sp-detail">
|
||||
<dl class="sp-kv">
|
||||
<dt>Expires</dt><dd><span class="sp-value">2026-10-03 09:14 UTC</span></dd>
|
||||
<dt>Names</dt><dd><span class="sp-value">mail.example.org</span></dd>
|
||||
</dl>
|
||||
<p class="sp-note">Renewal normally runs 30 days before expiry. Check that port 80 is reachable and look for <code>acme</code> lines in the <a href="#">system log</a>.</p>
|
||||
</div>
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary><strong>Machine</strong><span class="sp-muted">CPU 12 % · memory 41 % · ↓ 18 kB/s ↑ 44 kB/s</span><span class="sp-tag ok">ok</span></summary>
|
||||
<div class="sp-detail">
|
||||
<table>
|
||||
<thead><tr><th>Resource</th><th>Usage</th><th>Detail</th></tr></thead>
|
||||
<tbody>
|
||||
<tr><td>CPU</td><td><meter value="12" min="0" max="100" low="70" high="90" optimum="10"></meter>12 %</td><td class="sp-muted">2 cores, load 0.21</td></tr>
|
||||
<tr><td>Memory</td><td><meter value="41" min="0" max="100" low="70" high="90" optimum="10"></meter>41 %</td><td class="sp-muted">812 MB of 1.9 GB</td></tr>
|
||||
<tr><td>Network</td><td>↓ 18 kB/s · ↑ 44 kB/s</td><td class="sp-muted">eth0: 2.1 GB in, 6.4 GB out</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary><strong>Queues</strong><span class="sp-muted">Outbound empty · inbound empty</span><span class="sp-tag ok">ok</span></summary>
|
||||
<div class="sp-detail"><p>No messages are waiting. Open the <a href="#">outbound queue</a> or the <a href="#">inbound queue</a>.</p></div>
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary><strong>Milter sockets</strong><span class="sp-muted">2 of 2 answering</span><span class="sp-tag ok">ok</span></summary>
|
||||
<div class="sp-detail">
|
||||
<table>
|
||||
<thead><tr><th>Milter</th><th>State</th><th>Detail</th></tr></thead>
|
||||
<tbody>
|
||||
<tr><td>opendkim</td><td><span class="sp-tag ok">ok</span></td><td class="sp-muted">inet:127.0.0.1:8891</td></tr>
|
||||
<tr><td>journal-milter</td><td><span class="sp-tag ok">ok</span></td><td class="sp-muted">unix:/run/selfpost/journal.sock</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary><strong>Hostname and reverse DNS</strong><span class="sp-muted">mail.example.org ⇄ 203.0.113.25</span><span class="sp-tag ok">ok</span></summary>
|
||||
<div class="sp-detail">
|
||||
<dl class="sp-kv">
|
||||
<dt>Hostname</dt><dd><span class="sp-value">mail.example.org</span></dd>
|
||||
<dt>Lookup</dt><dd><span class="sp-value">mail.example.org → 203.0.113.25<br>203.0.113.25 → mail.example.org</span></dd>
|
||||
</dl>
|
||||
<form method="post" action="#"><button type="submit" class="outline secondary sp-btn-sm">Re-check DNS</button></form>
|
||||
</div>
|
||||
</details>
|
||||
</section>
|
||||
|
||||
<footer class="sp-foot">
|
||||
SelfPost 1.8.0 · © 2026 Mix · <a href="#">License (AGPL-3.0)</a> · <a href="#">Source</a> · No warranty
|
||||
</footer>
|
||||
</main>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,206 @@
|
||||
/* Direction A — "Blank" (the postal form). Pico CSS v2 carries every generic
|
||||
control (type, buttons, inputs, tables, details, dropdown); this file maps
|
||||
the brand onto Pico's variables and adds only what is SelfPost's own:
|
||||
the two-row header, the sheet, numbered form sections, the postmark and
|
||||
the DNS record block. */
|
||||
|
||||
/* :root[...] rather than [data-theme=light] alone: Pico declares its light
|
||||
palette on :root:not([data-theme=dark]), which a bare attribute selector
|
||||
loses to. */
|
||||
:root[data-theme=light] {
|
||||
--pico-font-family-sans-serif: var(--sp-sans);
|
||||
--pico-font-family-monospace: var(--sp-mono);
|
||||
--pico-font-family: var(--sp-sans);
|
||||
--pico-border-radius: 2px;
|
||||
--pico-spacing: .9rem;
|
||||
--pico-typography-spacing-vertical: .9rem;
|
||||
--pico-form-element-spacing-vertical: .5rem;
|
||||
--pico-form-element-spacing-horizontal: .75rem;
|
||||
|
||||
--pico-background-color: var(--sp-paper);
|
||||
--pico-color: var(--sp-ink);
|
||||
--pico-muted-color: var(--sp-muted);
|
||||
--pico-muted-border-color: var(--sp-rule);
|
||||
--pico-h1-color: var(--sp-ink); --pico-h2-color: var(--sp-ink);
|
||||
--pico-h3-color: var(--sp-ink); --pico-h4-color: var(--sp-ink);
|
||||
|
||||
--pico-primary: var(--sp-brick);
|
||||
--pico-primary-background: var(--sp-brick);
|
||||
--pico-primary-border: var(--sp-brick);
|
||||
--pico-primary-underline: rgb(122 59 46 / .35);
|
||||
--pico-primary-hover: var(--sp-brick-dark);
|
||||
--pico-primary-hover-background: var(--sp-brick-dark);
|
||||
--pico-primary-hover-border: var(--sp-brick-dark);
|
||||
--pico-primary-hover-underline: var(--sp-brick-dark);
|
||||
--pico-primary-focus: rgb(122 59 46 / .25);
|
||||
--pico-primary-inverse: #fff;
|
||||
|
||||
--pico-secondary: var(--sp-ink-2);
|
||||
--pico-secondary-background: var(--sp-ink-2);
|
||||
--pico-secondary-border: var(--sp-ink-2);
|
||||
--pico-secondary-hover: var(--sp-ink);
|
||||
--pico-secondary-hover-background: var(--sp-ink);
|
||||
--pico-secondary-hover-border: var(--sp-ink);
|
||||
--pico-secondary-focus: rgb(18 22 28 / .2);
|
||||
--pico-secondary-inverse: #fff;
|
||||
|
||||
--pico-form-element-background-color: #fff;
|
||||
--pico-form-element-border-color: var(--sp-rule-strong);
|
||||
--pico-form-element-active-border-color: var(--sp-brick);
|
||||
--pico-form-element-focus-color: rgb(122 59 46 / .25);
|
||||
--pico-table-border-color: var(--sp-rule);
|
||||
--pico-code-background-color: var(--sp-well);
|
||||
--pico-code-color: var(--sp-ink);
|
||||
--pico-dropdown-background-color: #fff;
|
||||
--pico-dropdown-border-color: var(--sp-rule);
|
||||
--pico-dropdown-hover-background-color: var(--sp-brick-tint);
|
||||
}
|
||||
|
||||
/* Pico grows the root font with the viewport (up to 131%). An ops panel wants
|
||||
one size everywhere. */
|
||||
:root { font-size: 15px; }
|
||||
body { min-height: 100vh; display: flex; flex-direction: column; }
|
||||
h1 { font-size: 1.7rem; font-weight: 600; margin: 0; }
|
||||
small, .sp-muted { color: var(--sp-muted); }
|
||||
code, .sp-mono { font-family: var(--sp-mono); font-size: .88em; }
|
||||
|
||||
/* ---- header: sections on the first row, the section's pages on the second */
|
||||
.sp-wrap { width: 100%; max-width: 72rem; margin-inline: auto; padding-inline: 1.5rem; }
|
||||
.sp-bar { background: var(--sp-sheet); border-bottom: 1px solid var(--sp-rule); }
|
||||
.sp-bar nav { align-items: center; }
|
||||
.sp-bar nav li { padding-block: .45rem; }
|
||||
.sp-brand img { height: 46px; width: auto; display: block; }
|
||||
.sp-sections a {
|
||||
font-family: var(--sp-mono); font-size: .8rem; font-weight: 600;
|
||||
letter-spacing: .09em; text-transform: uppercase; text-decoration: none;
|
||||
color: var(--sp-muted); padding: .5rem .8rem; margin: 0; border-radius: 0;
|
||||
}
|
||||
.sp-sections a:hover { color: var(--sp-ink); }
|
||||
.sp-sections a[aria-current] { color: var(--sp-brick); box-shadow: inset 0 -3px 0 var(--sp-brick); }
|
||||
.sp-user summary { font-size: .9rem; }
|
||||
.sp-pages { background: var(--sp-paper); border-bottom: 1px solid var(--sp-rule); }
|
||||
.sp-pages .sp-wrap { display: flex; gap: 1.6rem; }
|
||||
.sp-pages a { color: var(--sp-muted); text-decoration: none; padding: .6rem 0; font-size: .95rem; }
|
||||
.sp-pages a:hover { color: var(--sp-ink); }
|
||||
.sp-pages a[aria-current] { color: var(--sp-ink); font-weight: 600; box-shadow: inset 0 -2px 0 var(--sp-ink); }
|
||||
|
||||
/* ---- the sheet: one column, one width, every page */
|
||||
main.sp-wrap { flex: 1; padding-block: 1.8rem 2.5rem; }
|
||||
.sp-head { display: flex; align-items: flex-end; justify-content: space-between; gap: 1rem; flex-wrap: wrap; margin-bottom: 1.4rem; }
|
||||
.sp-head .sp-kicker { font-family: var(--sp-mono); font-size: .75rem; letter-spacing: .1em; text-transform: uppercase; color: var(--sp-muted); margin: 0 0 .15rem; }
|
||||
.sp-head .sp-kicker a { color: inherit; }
|
||||
.sp-head-actions { display: flex; gap: .5rem; align-items: center; }
|
||||
.sp-head-actions > * { margin: 0; }
|
||||
.sp-sheet { background: var(--sp-sheet); border: 1px solid var(--sp-rule); margin-bottom: 1.4rem; }
|
||||
.sp-sheet > header {
|
||||
display: flex; align-items: baseline; gap: .8rem; padding: .7rem 1.2rem;
|
||||
border-bottom: 1px solid var(--sp-rule);
|
||||
}
|
||||
.sp-sheet > header .sp-no { font-family: var(--sp-mono); font-size: .75rem; color: var(--sp-brick); font-weight: 600; }
|
||||
.sp-sheet > header h2 {
|
||||
font-family: var(--sp-mono); font-size: .8rem; font-weight: 600; letter-spacing: .1em;
|
||||
text-transform: uppercase; margin: 0; flex: 1;
|
||||
}
|
||||
.sp-sheet > header small { font-size: .85rem; }
|
||||
.sp-sheet > .sp-body { padding: 1.1rem 1.2rem; }
|
||||
.sp-sheet > .sp-body > :last-child { margin-bottom: 0; }
|
||||
.sp-foot { color: var(--sp-muted); font-size: .8rem; border-top: 1px solid var(--sp-rule); padding-block: 1rem; }
|
||||
.sp-foot a { color: inherit; }
|
||||
|
||||
/* ---- status tag and the postmark */
|
||||
.sp-tag {
|
||||
display: inline-block; font-family: var(--sp-mono); font-size: .72rem; font-weight: 600;
|
||||
letter-spacing: .08em; text-transform: uppercase; padding: .1rem .5rem;
|
||||
border: 1px solid var(--sp-idle-rule); background: var(--sp-idle-bg); color: var(--sp-idle);
|
||||
border-radius: 2px; white-space: nowrap; text-decoration: none;
|
||||
}
|
||||
.sp-tag.ok { color: var(--sp-ok); background: var(--sp-ok-bg); border-color: var(--sp-ok-rule); }
|
||||
.sp-tag.warn { color: var(--sp-warn); background: var(--sp-warn-bg); border-color: var(--sp-warn-rule); }
|
||||
.sp-tag.fail { color: var(--sp-fail); background: var(--sp-fail-bg); border-color: var(--sp-fail-rule); }
|
||||
|
||||
.sp-verdict { display: flex; align-items: center; gap: 1.5rem; padding: 1.3rem 1.4rem; }
|
||||
.sp-verdict h2 { margin: 0 0 .2rem; font-size: 1.35rem; font-weight: 600; }
|
||||
.sp-verdict p { margin: 0; color: var(--sp-muted); }
|
||||
.sp-postmark {
|
||||
flex: none; width: 5.6rem; height: 5.6rem; border-radius: 50%;
|
||||
border: 2px solid currentColor; outline: 1px solid currentColor; outline-offset: 3px;
|
||||
display: grid; place-content: center; text-align: center; transform: rotate(-9deg);
|
||||
font-family: var(--sp-mono); font-weight: 600; letter-spacing: .08em; line-height: 1.15;
|
||||
color: var(--sp-ok);
|
||||
}
|
||||
.sp-postmark.warn { color: var(--sp-warn); }
|
||||
.sp-postmark.fail { color: var(--sp-fail); }
|
||||
.sp-postmark b { font-size: 1.25rem; display: block; }
|
||||
.sp-postmark span { font-size: .55rem; display: block; }
|
||||
|
||||
/* ---- ledger: one row per check, the row opens into its detail */
|
||||
.sp-ledger details { margin: 0; padding: 0; border-bottom: 1px solid var(--sp-rule); }
|
||||
.sp-ledger details:last-child { border-bottom: 0; }
|
||||
.sp-ledger summary {
|
||||
display: grid; grid-template-columns: 13rem 1fr auto 1.2rem; gap: 1rem; align-items: center;
|
||||
padding: .75rem 1.2rem; margin: 0; color: var(--sp-ink);
|
||||
}
|
||||
.sp-ledger summary::after { grid-column: 4; margin: 0; float: none; }
|
||||
.sp-ledger summary:hover { background: var(--sp-paper); }
|
||||
.sp-ledger summary strong { font-weight: 600; }
|
||||
.sp-ledger summary span.sp-muted { font-size: .93rem; }
|
||||
.sp-ledger .sp-detail { padding: .2rem 1.2rem 1.1rem 15.2rem; }
|
||||
.sp-ledger .sp-detail table { margin: 0; font-size: .93rem; }
|
||||
.sp-ledger .sp-detail > :last-child { margin-bottom: 0; }
|
||||
.sp-ledger meter { width: 9rem; height: .7rem; vertical-align: middle; margin-right: .5rem; }
|
||||
|
||||
/* ---- label / value / copy: the DNS record block and connection settings */
|
||||
.sp-record { padding: 1.1rem 1.2rem; border-bottom: 1px solid var(--sp-rule); }
|
||||
.sp-record:last-child { border-bottom: 0; }
|
||||
.sp-record-head { display: flex; align-items: center; gap: .7rem; margin-bottom: .7rem; }
|
||||
.sp-record-head h3 { margin: 0; font-size: 1.05rem; font-weight: 600; }
|
||||
.sp-record-head small { margin-left: auto; }
|
||||
.sp-kv { display: grid; grid-template-columns: 6.5rem 1fr; gap: .45rem 1rem; align-items: start; margin: 0; }
|
||||
.sp-kv dt {
|
||||
font-family: var(--sp-mono); font-size: .72rem; letter-spacing: .08em; text-transform: uppercase;
|
||||
color: var(--sp-muted); padding-top: .5rem; margin: 0;
|
||||
}
|
||||
.sp-kv dd { margin: 0; display: flex; gap: .4rem; align-items: stretch; min-width: 0; }
|
||||
.sp-value {
|
||||
flex: 1; min-width: 0; font-family: var(--sp-mono); font-size: .86rem; line-height: 1.5;
|
||||
background: var(--sp-well); border: 1px solid var(--sp-rule); padding: .4rem .65rem;
|
||||
overflow-wrap: anywhere; border-radius: 2px;
|
||||
}
|
||||
.sp-value.plain { background: none; border-color: transparent; padding-left: 0; font-family: var(--sp-sans); font-size: .95rem; }
|
||||
.sp-copy {
|
||||
flex: none; align-self: flex-start; height: 2.15rem; margin: 0; padding: 0 .7rem; font-size: .8rem;
|
||||
--pico-background-color: #fff; --pico-border-color: var(--sp-rule-strong); --pico-color: var(--sp-ink-2);
|
||||
}
|
||||
.sp-copy:hover { --pico-background-color: var(--sp-paper); --pico-border-color: var(--sp-ink-2); --pico-color: var(--sp-ink); }
|
||||
.sp-note { margin: .7rem 0 0 7.5rem; font-size: .9rem; color: var(--sp-muted); }
|
||||
.sp-note.fail { color: var(--sp-fail); }
|
||||
.sp-cols { display: grid; grid-template-columns: repeat(auto-fit, minmax(20rem, 1fr)); gap: 0 2.4rem; }
|
||||
|
||||
/* ---- applications */
|
||||
.sp-apps { margin: 0; font-size: .95rem; }
|
||||
.sp-apps td, .sp-apps th { padding-inline: 1.2rem .4rem; vertical-align: middle; }
|
||||
.sp-apps th { font-family: var(--sp-mono); font-size: .72rem; letter-spacing: .08em; text-transform: uppercase; color: var(--sp-muted); font-weight: 400; }
|
||||
.sp-apps td:last-child { text-align: right; padding-right: 1.2rem; white-space: nowrap; }
|
||||
.sp-apps .sp-login { font-family: var(--sp-mono); font-weight: 600; }
|
||||
.sp-link-btn { font-size: .9rem; margin-left: .9rem; }
|
||||
.sp-link-btn.danger { color: var(--sp-fail); --pico-underline: rgb(180 35 24 / .35); text-decoration-color: rgb(180 35 24 / .35); }
|
||||
.sp-btn-sm { padding: .35rem .8rem; font-size: .9rem; margin: 0; width: auto; }
|
||||
|
||||
/* ---- sign-in */
|
||||
.sp-signin { flex: 1; display: grid; place-content: center; padding: 2rem 1.5rem; }
|
||||
.sp-signin-card { width: 23rem; max-width: 100%; }
|
||||
.sp-signin-card img { display: block; width: 15rem; height: auto; margin: 0 auto 1.6rem; }
|
||||
.sp-signin-card .sp-body { padding: 1.4rem 1.4rem 1.2rem; }
|
||||
.sp-signin-card button { margin-bottom: 0; }
|
||||
.sp-tagline { text-align: center; font-family: var(--sp-mono); font-size: .75rem; letter-spacing: .16em; color: var(--sp-muted); margin-top: 1.2rem; }
|
||||
|
||||
@media (max-width: 48rem) {
|
||||
.sp-ledger summary { grid-template-columns: 1fr auto 1.2rem; }
|
||||
.sp-ledger summary span.sp-muted { grid-column: 1 / -1; grid-row: 2; }
|
||||
.sp-ledger summary::after { grid-column: 3; }
|
||||
.sp-ledger .sp-detail { padding-left: 1.2rem; }
|
||||
.sp-kv { grid-template-columns: 1fr; gap: .15rem; }
|
||||
.sp-kv dt { padding-top: .5rem; }
|
||||
.sp-note { margin-left: 0; }
|
||||
.sp-pages .sp-wrap { overflow-x: auto; white-space: nowrap; }
|
||||
}
|
||||
@@ -0,0 +1,212 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en" data-bs-theme="light">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>example.org · Outbound domains · SelfPost</title>
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@tabler/core@1.5.1/dist/css/tabler.min.css">
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@tabler/icons-webfont@3/dist/tabler-icons.min.css">
|
||||
<link rel="stylesheet" href="../shared/brand.css">
|
||||
<link rel="stylesheet" href="theme.css">
|
||||
</head>
|
||||
<body>
|
||||
<div class="page">
|
||||
|
||||
<aside class="navbar navbar-vertical navbar-expand-lg" data-bs-theme="dark">
|
||||
<div class="container-fluid">
|
||||
<a class="navbar-brand" href="status.html"><img src="../../selfpost-stamp-compact.svg" alt="SelfPost"></a>
|
||||
<div class="navbar-collapse collapse show">
|
||||
<ul class="navbar-nav">
|
||||
<li class="nav-item"><a class="nav-link" href="status.html"><span class="nav-link-icon"><i class="ti ti-activity-heartbeat"></i></span><span class="nav-link-title">Overview</span></a></li>
|
||||
|
||||
<li class="sp-nav-label">Outbound</li>
|
||||
<li class="nav-item active"><a class="nav-link" href="domain.html"><span class="nav-link-icon"><i class="ti ti-world-upload"></i></span><span class="nav-link-title">Domains</span></a></li>
|
||||
<li class="nav-item"><a class="nav-link" href="#"><span class="nav-link-icon"><i class="ti ti-list-details"></i></span><span class="nav-link-title">Log</span></a></li>
|
||||
<li class="nav-item"><a class="nav-link" href="#"><span class="nav-link-icon"><i class="ti ti-stack-2"></i></span><span class="nav-link-title">Queue</span></a></li>
|
||||
<li class="nav-item"><a class="nav-link" href="#"><span class="nav-link-icon"><i class="ti ti-report-analytics"></i></span><span class="nav-link-title">DMARC reports</span></a></li>
|
||||
|
||||
<li class="sp-nav-label">Inbound</li>
|
||||
<li class="nav-item"><a class="nav-link" href="#"><span class="nav-link-icon"><i class="ti ti-world-download"></i></span><span class="nav-link-title">Domains</span></a></li>
|
||||
<li class="nav-item"><a class="nav-link" href="#"><span class="nav-link-icon"><i class="ti ti-list-details"></i></span><span class="nav-link-title">Log</span></a></li>
|
||||
<li class="nav-item"><a class="nav-link" href="#"><span class="nav-link-icon"><i class="ti ti-stack-2"></i></span><span class="nav-link-title">Queue</span></a></li>
|
||||
|
||||
<li class="sp-nav-label">Server</li>
|
||||
<li class="nav-item"><a class="nav-link" href="#"><span class="nav-link-icon"><i class="ti ti-file-text"></i></span><span class="nav-link-title">System log</span></a></li>
|
||||
<li class="nav-item"><a class="nav-link" href="#"><span class="nav-link-icon"><i class="ti ti-archive"></i></span><span class="nav-link-title">Backup</span></a></li>
|
||||
<li class="nav-item"><a class="nav-link" href="#"><span class="nav-link-icon"><i class="ti ti-users"></i></span><span class="nav-link-title">Users</span></a></li>
|
||||
<li class="nav-item"><a class="nav-link" href="#"><span class="nav-link-icon"><i class="ti ti-settings"></i></span><span class="nav-link-title">Settings</span></a></li>
|
||||
</ul>
|
||||
<ul class="navbar-nav sp-nav-foot">
|
||||
<li class="sp-nav-user">Signed in as admin</li>
|
||||
<li class="nav-item"><a class="nav-link" href="#"><span class="nav-link-icon"><i class="ti ti-user-circle"></i></span><span class="nav-link-title">Account</span></a></li>
|
||||
<li class="nav-item"><a class="nav-link" href="#"><span class="nav-link-icon"><i class="ti ti-help"></i></span><span class="nav-link-title">Help</span></a></li>
|
||||
<li class="nav-item"><a class="nav-link" href="login.html"><span class="nav-link-icon"><i class="ti ti-logout"></i></span><span class="nav-link-title">Sign out</span></a></li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
<div class="page-wrapper">
|
||||
<div class="page-header">
|
||||
<div class="container-xl">
|
||||
<div class="row g-2 align-items-center">
|
||||
<div class="col">
|
||||
<div class="page-pretitle"><a href="#" class="text-reset">Outbound domains</a> /</div>
|
||||
<h2 class="page-title">example.org <span class="badge bg-red-lt ms-2">DNS: 1 mismatch</span></h2>
|
||||
</div>
|
||||
<div class="col-auto">
|
||||
<div class="btn-list">
|
||||
<form method="post" action="#"><button type="submit" class="btn"><i class="ti ti-refresh me-1"></i>Re-check DNS</button></form>
|
||||
<a href="#" class="btn btn-primary"><i class="ti ti-plus me-1"></i>Add application</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="page-body">
|
||||
<div class="container-xl">
|
||||
<div class="row row-cards">
|
||||
|
||||
<div class="col-lg-8">
|
||||
<div class="card">
|
||||
<div class="card-header"><h3 class="card-title">DNS records</h3><div class="card-actions text-secondary small">Checked 3 min ago</div></div>
|
||||
|
||||
<div class="card-body sp-record">
|
||||
<div class="d-flex align-items-center mb-3">
|
||||
<h4 class="m-0 me-2">DKIM</h4><span class="badge bg-green-lt">published</span>
|
||||
<span class="ms-auto text-secondary small">Not a secret · selector <code>sp2026</code></span>
|
||||
</div>
|
||||
<div class="row g-3">
|
||||
<div class="col-md-9">
|
||||
<label class="form-label">Host / name</label>
|
||||
<div class="input-group"><input class="form-control" readonly value="sp2026._domainkey.example.org"><button class="btn" type="button"><i class="ti ti-copy me-1"></i>Copy</button></div>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<label class="form-label">Type</label>
|
||||
<input class="form-control" readonly value="TXT">
|
||||
</div>
|
||||
<div class="col-12">
|
||||
<label class="form-label">Value to publish</label>
|
||||
<div class="input-group"><textarea class="form-control" rows="3" readonly>v=DKIM1; k=rsa; p=MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAx3kqV0mJd8e2f1Yb7Qw5uT9sLr4nHc6pZaXo1vKe0yWm2gBt8RjN5dCq3hUf7lPs9aEi4oMz6xTb1kGv0nYw…IDAQAB</textarea><button class="btn" type="button"><i class="ti ti-copy me-1"></i>Copy</button></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card-body sp-record">
|
||||
<div class="d-flex align-items-center mb-3">
|
||||
<h4 class="m-0 me-2">SPF</h4><span class="badge bg-red-lt">mismatch</span>
|
||||
<span class="ms-auto text-secondary small">Merge into the existing record — never publish a second one</span>
|
||||
</div>
|
||||
<div class="row g-3">
|
||||
<div class="col-md-9">
|
||||
<label class="form-label">Host / name</label>
|
||||
<div class="input-group"><input class="form-control" readonly value="example.org"><button class="btn" type="button"><i class="ti ti-copy me-1"></i>Copy</button></div>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<label class="form-label">Type</label>
|
||||
<input class="form-control" readonly value="TXT">
|
||||
</div>
|
||||
<div class="col-12">
|
||||
<label class="form-label">Value to publish</label>
|
||||
<div class="input-group"><input class="form-control" readonly value="v=spf1 ip4:203.0.113.25 ~all"><button class="btn" type="button"><i class="ti ti-copy me-1"></i>Copy</button></div>
|
||||
</div>
|
||||
<div class="col-12">
|
||||
<label class="form-label">In DNS now</label>
|
||||
<input class="form-control" readonly value="v=spf1 include:_spf.google.com ~all">
|
||||
<div class="text-danger small mt-2">The published record does not authorize 203.0.113.25. Add <code>ip4:203.0.113.25</code> before <code>~all</code>.</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card-body sp-record">
|
||||
<div class="d-flex align-items-center mb-3">
|
||||
<h4 class="m-0 me-2">DMARC</h4><span class="badge bg-green-lt">published</span>
|
||||
<span class="ms-auto text-secondary small">Report address from <a href="#">Settings</a></span>
|
||||
</div>
|
||||
<div class="row g-3">
|
||||
<div class="col-md-9">
|
||||
<label class="form-label">Host / name</label>
|
||||
<div class="input-group"><input class="form-control" readonly value="_dmarc.example.org"><button class="btn" type="button"><i class="ti ti-copy me-1"></i>Copy</button></div>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<label class="form-label">Type</label>
|
||||
<input class="form-control" readonly value="TXT">
|
||||
</div>
|
||||
<div class="col-12">
|
||||
<label class="form-label">Value to publish</label>
|
||||
<div class="input-group"><input class="form-control" readonly value="v=DMARC1; p=none; rua=mailto:dmarc@mail.example.org"><button class="btn" type="button"><i class="ti ti-copy me-1"></i>Copy</button></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-lg-4">
|
||||
<div class="card mb-3">
|
||||
<div class="card-header"><h3 class="card-title">Connection</h3></div>
|
||||
<div class="card-body">
|
||||
<div class="datagrid">
|
||||
<div class="datagrid-item"><div class="datagrid-title">Server</div><div class="datagrid-content font-monospace">mail.example.org</div></div>
|
||||
<div class="datagrid-item"><div class="datagrid-title">Ports</div><div class="datagrid-content font-monospace">465 SSL/TLS<br>587 STARTTLS</div></div>
|
||||
</div>
|
||||
<p class="text-secondary small mt-3 mb-0">The same for every domain. Authenticate with an application login.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card mb-3">
|
||||
<div class="card-header"><h3 class="card-title">Sent, 30 days</h3></div>
|
||||
<div class="card-body">
|
||||
<div class="datagrid">
|
||||
<div class="datagrid-item"><div class="datagrid-title">Messages</div><div class="datagrid-content h2 m-0">1 284</div></div>
|
||||
<div class="datagrid-item"><div class="datagrid-title">Peak</div><div class="datagrid-content h2 m-0">96 <span class="text-secondary fs-5">/h</span></div></div>
|
||||
<div class="datagrid-item"><div class="datagrid-title">Average</div><div class="datagrid-content h2 m-0">1.8 <span class="text-secondary fs-5">/h</span></div></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<div class="card-header"><h3 class="card-title">Domain settings</h3></div>
|
||||
<div class="list-group list-group-flush">
|
||||
<a href="#" class="list-group-item list-group-item-action d-flex align-items-center"><div><div>DMARC reports</div><div class="text-secondary small">Same as Settings</div></div><i class="ti ti-chevron-right ms-auto text-secondary"></i></a>
|
||||
<a href="#" class="list-group-item list-group-item-action d-flex align-items-center"><div><div>Rate limit <span class="badge bg-green-lt ms-1">active</span></div><div class="text-secondary small">Auto ×3 — 288 messages / hour</div></div><i class="ti ti-chevron-right ms-auto text-secondary"></i></a>
|
||||
<a href="#" class="list-group-item list-group-item-action d-flex align-items-center"><div><div>Export</div><div class="text-secondary small">Key and applications as one file</div></div><i class="ti ti-chevron-right ms-auto text-secondary"></i></a>
|
||||
<a href="#" class="list-group-item list-group-item-action d-flex align-items-center text-danger"><div>Delete domain</div><i class="ti ti-chevron-right ms-auto"></i></a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-12">
|
||||
<div class="card">
|
||||
<div class="card-header"><h3 class="card-title">Applications</h3><div class="card-actions text-secondary small">SASL logins that may send as this domain</div></div>
|
||||
<div class="table-responsive">
|
||||
<table class="table table-vcenter card-table">
|
||||
<thead><tr><th>Login</th><th>May send as</th><th>30 days</th><th>Limits</th><th class="w-1"></th></tr></thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td class="font-monospace fw-semibold">prod-server</td><td>*@example.org</td><td class="text-secondary">1 102 msg · peak 96/h</td>
|
||||
<td><span class="badge bg-green-lt">200 / h</span></td>
|
||||
<td><div class="btn-list flex-nowrap"><a href="#" class="btn btn-sm">Edit</a><a href="#" class="btn btn-sm">New password</a><a href="#" class="btn btn-sm btn-ghost-danger">Delete</a></div></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="font-monospace fw-semibold">alerts</td><td>alerts@example.org, noc@example.org</td><td class="text-secondary">182 msg · peak 14/h</td>
|
||||
<td><span class="badge bg-secondary-lt">domain</span> <span class="badge bg-green-lt">2 IPs</span></td>
|
||||
<td><div class="btn-list flex-nowrap"><a href="#" class="btn btn-sm">Edit</a><a href="#" class="btn btn-sm">New password</a><a href="#" class="btn btn-sm btn-ghost-danger">Delete</a></div></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<footer class="footer footer-transparent">
|
||||
<div class="container-xl sp-foot">SelfPost 1.8.0 · © 2026 Mix · <a href="#">License (AGPL-3.0)</a> · <a href="#">Source</a> · No warranty</div>
|
||||
</footer>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,36 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en" data-bs-theme="light">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>Sign in · SelfPost</title>
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@tabler/core@1.5.1/dist/css/tabler.min.css">
|
||||
<link rel="stylesheet" href="../shared/brand.css">
|
||||
<link rel="stylesheet" href="theme.css">
|
||||
</head>
|
||||
<body class="d-flex flex-column">
|
||||
<div class="page page-center">
|
||||
<div class="container container-tight py-4">
|
||||
<div class="text-center mb-4"><img class="sp-signin-mark" src="../../selfpost-stamp.svg" alt="SelfPost"></div>
|
||||
<div class="card card-md">
|
||||
<div class="card-body">
|
||||
<h2 class="h2 text-center mb-4">Sign in</h2>
|
||||
<form method="post" action="status.html">
|
||||
<div class="mb-3">
|
||||
<label class="form-label" for="username">Username</label>
|
||||
<input class="form-control" id="username" name="username" autocomplete="username" required>
|
||||
</div>
|
||||
<div class="mb-2">
|
||||
<label class="form-label" for="password">Password</label>
|
||||
<input class="form-control" id="password" name="password" type="password" autocomplete="current-password" required>
|
||||
</div>
|
||||
<div class="form-footer"><button type="submit" class="btn btn-primary w-100">Sign in</button></div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
<div class="text-center sp-tagline mt-4">YOUR SMTP. YOUR RULES.</div>
|
||||
<div class="text-center sp-foot mt-3">© 2026 Mix · <a href="#">License (AGPL-3.0)</a> · <a href="#">Source</a> · No warranty</div>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,168 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en" data-bs-theme="light">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>Overview · SelfPost</title>
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@tabler/core@1.5.1/dist/css/tabler.min.css">
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@tabler/icons-webfont@3/dist/tabler-icons.min.css">
|
||||
<link rel="stylesheet" href="../shared/brand.css">
|
||||
<link rel="stylesheet" href="theme.css">
|
||||
</head>
|
||||
<body>
|
||||
<div class="page">
|
||||
|
||||
<aside class="navbar navbar-vertical navbar-expand-lg" data-bs-theme="dark">
|
||||
<div class="container-fluid">
|
||||
<a class="navbar-brand" href="status.html"><img src="../../selfpost-stamp-compact.svg" alt="SelfPost"></a>
|
||||
<div class="navbar-collapse collapse show">
|
||||
<ul class="navbar-nav">
|
||||
<li class="nav-item active"><a class="nav-link" href="status.html"><span class="nav-link-icon"><i class="ti ti-activity-heartbeat"></i></span><span class="nav-link-title">Overview</span></a></li>
|
||||
|
||||
<li class="sp-nav-label">Outbound</li>
|
||||
<li class="nav-item"><a class="nav-link" href="domain.html"><span class="nav-link-icon"><i class="ti ti-world-upload"></i></span><span class="nav-link-title">Domains</span></a></li>
|
||||
<li class="nav-item"><a class="nav-link" href="#"><span class="nav-link-icon"><i class="ti ti-list-details"></i></span><span class="nav-link-title">Log</span></a></li>
|
||||
<li class="nav-item"><a class="nav-link" href="#"><span class="nav-link-icon"><i class="ti ti-stack-2"></i></span><span class="nav-link-title">Queue</span></a></li>
|
||||
<li class="nav-item"><a class="nav-link" href="#"><span class="nav-link-icon"><i class="ti ti-report-analytics"></i></span><span class="nav-link-title">DMARC reports</span></a></li>
|
||||
|
||||
<li class="sp-nav-label">Inbound</li>
|
||||
<li class="nav-item"><a class="nav-link" href="#"><span class="nav-link-icon"><i class="ti ti-world-download"></i></span><span class="nav-link-title">Domains</span></a></li>
|
||||
<li class="nav-item"><a class="nav-link" href="#"><span class="nav-link-icon"><i class="ti ti-list-details"></i></span><span class="nav-link-title">Log</span></a></li>
|
||||
<li class="nav-item"><a class="nav-link" href="#"><span class="nav-link-icon"><i class="ti ti-stack-2"></i></span><span class="nav-link-title">Queue</span></a></li>
|
||||
|
||||
<li class="sp-nav-label">Server</li>
|
||||
<li class="nav-item"><a class="nav-link" href="#"><span class="nav-link-icon"><i class="ti ti-file-text"></i></span><span class="nav-link-title">System log</span></a></li>
|
||||
<li class="nav-item"><a class="nav-link" href="#"><span class="nav-link-icon"><i class="ti ti-archive"></i></span><span class="nav-link-title">Backup</span></a></li>
|
||||
<li class="nav-item"><a class="nav-link" href="#"><span class="nav-link-icon"><i class="ti ti-users"></i></span><span class="nav-link-title">Users</span></a></li>
|
||||
<li class="nav-item"><a class="nav-link" href="#"><span class="nav-link-icon"><i class="ti ti-settings"></i></span><span class="nav-link-title">Settings</span></a></li>
|
||||
</ul>
|
||||
<ul class="navbar-nav sp-nav-foot">
|
||||
<li class="sp-nav-user">Signed in as admin</li>
|
||||
<li class="nav-item"><a class="nav-link" href="#"><span class="nav-link-icon"><i class="ti ti-user-circle"></i></span><span class="nav-link-title">Account</span></a></li>
|
||||
<li class="nav-item"><a class="nav-link" href="#"><span class="nav-link-icon"><i class="ti ti-help"></i></span><span class="nav-link-title">Help</span></a></li>
|
||||
<li class="nav-item"><a class="nav-link" href="login.html"><span class="nav-link-icon"><i class="ti ti-logout"></i></span><span class="nav-link-title">Sign out</span></a></li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
<div class="page-wrapper">
|
||||
<div class="page-header">
|
||||
<div class="container-xl">
|
||||
<div class="row g-2 align-items-center">
|
||||
<div class="col">
|
||||
<div class="page-pretitle">mail.example.org</div>
|
||||
<h2 class="page-title">Overview</h2>
|
||||
</div>
|
||||
<div class="col-auto text-secondary small">Updated 14:02:31 UTC · refreshes every 10 s</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="page-body">
|
||||
<div class="container-xl">
|
||||
|
||||
<div class="alert alert-warning bg-white" role="alert">
|
||||
<div class="d-flex">
|
||||
<div><i class="ti ti-alert-triangle alert-icon"></i></div>
|
||||
<div>
|
||||
<h4 class="alert-title">Mail is flowing. One thing needs attention.</h4>
|
||||
<div class="text-secondary">The TLS certificate expires in 12 days and has not renewed yet.</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row row-cards mb-3">
|
||||
<div class="col-sm-6 col-lg-4"><div class="card card-sm sp-tile"><div class="card-body"><i class="ti ti-cpu sp-tile-icon"></i><div><div class="fw-semibold">Machine</div><div class="text-secondary small">CPU 12 % · mem 41 %</div></div><span class="status status-green"><span class="status-dot"></span>OK</span></div></div></div>
|
||||
<div class="col-sm-6 col-lg-4"><div class="card card-sm sp-tile"><div class="card-body"><i class="ti ti-server-cog sp-tile-icon"></i><div><div class="fw-semibold">Processes</div><div class="text-secondary small">5 of 5 running</div></div><span class="status status-green"><span class="status-dot"></span>OK</span></div></div></div>
|
||||
<div class="col-sm-6 col-lg-4"><div class="card card-sm sp-tile"><div class="card-body"><i class="ti ti-stack-2 sp-tile-icon"></i><div><div class="fw-semibold">Queues</div><div class="text-secondary small">Both empty</div></div><span class="status status-green"><span class="status-dot"></span>OK</span></div></div></div>
|
||||
<div class="col-sm-6 col-lg-4"><div class="card card-sm sp-tile"><div class="card-body"><i class="ti ti-certificate sp-tile-icon"></i><div><div class="fw-semibold">TLS certificate</div><div class="text-secondary small">Expires in 12 days</div></div><span class="status status-yellow"><span class="status-dot"></span>Warn</span></div></div></div>
|
||||
<div class="col-sm-6 col-lg-4"><div class="card card-sm sp-tile"><div class="card-body"><i class="ti ti-plug-connected sp-tile-icon"></i><div><div class="fw-semibold">Milter sockets</div><div class="text-secondary small">2 of 2 answering</div></div><span class="status status-green"><span class="status-dot"></span>OK</span></div></div></div>
|
||||
<div class="col-sm-6 col-lg-4"><div class="card card-sm sp-tile"><div class="card-body"><i class="ti ti-arrows-exchange sp-tile-icon"></i><div><div class="fw-semibold">Reverse DNS</div><div class="text-secondary small">Forward = reverse</div></div><span class="status status-green"><span class="status-dot"></span>OK</span></div></div></div>
|
||||
</div>
|
||||
|
||||
<div class="row row-deck row-cards">
|
||||
<div class="col-lg-6">
|
||||
<div class="card">
|
||||
<div class="card-header"><h3 class="card-title">Machine</h3><div class="card-actions"><span class="badge bg-green-lt">ok</span></div></div>
|
||||
<div class="table-responsive">
|
||||
<table class="table table-vcenter card-table">
|
||||
<thead><tr><th>Resource</th><th>Usage</th><th>Detail</th></tr></thead>
|
||||
<tbody>
|
||||
<tr><td>CPU</td><td><progress class="sp-bar" value="12" max="100"></progress>12 %</td><td class="text-secondary">2 cores, load 0.21</td></tr>
|
||||
<tr><td>Memory</td><td><progress class="sp-bar" value="41" max="100"></progress>41 %</td><td class="text-secondary">812 MB of 1.9 GB</td></tr>
|
||||
<tr><td>Network</td><td>↓ 18 kB/s · ↑ 44 kB/s</td><td class="text-secondary">eth0: 2.1 GB in, 6.4 GB out</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-lg-6">
|
||||
<div class="card">
|
||||
<div class="card-header"><h3 class="card-title">Processes</h3><div class="card-actions"><span class="badge bg-green-lt">ok</span></div></div>
|
||||
<div class="table-responsive">
|
||||
<table class="table table-vcenter card-table">
|
||||
<thead><tr><th>Program</th><th>State</th><th>Detail</th></tr></thead>
|
||||
<tbody>
|
||||
<tr><td>postfix</td><td><span class="badge bg-green-lt">running</span></td><td class="text-secondary">pid 41, uptime 18 days</td></tr>
|
||||
<tr><td>opendkim</td><td><span class="badge bg-green-lt">running</span></td><td class="text-secondary">pid 39, uptime 18 days</td></tr>
|
||||
<tr><td>journal-milter</td><td><span class="badge bg-green-lt">running</span></td><td class="text-secondary">pid 40, uptime 18 days</td></tr>
|
||||
<tr><td>panel</td><td><span class="badge bg-green-lt">running</span></td><td class="text-secondary">pid 44, uptime 18 days</td></tr>
|
||||
<tr><td>dmarc-ingest</td><td><span class="badge bg-green-lt">running</span></td><td class="text-secondary">pid 47, uptime 18 days</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-lg-4">
|
||||
<div class="card">
|
||||
<div class="card-header"><h3 class="card-title">TLS certificate</h3><div class="card-actions"><span class="badge bg-yellow-lt">warn</span></div></div>
|
||||
<div class="card-body">
|
||||
<div class="datagrid">
|
||||
<div class="datagrid-item"><div class="datagrid-title">Expires</div><div class="datagrid-content font-monospace">2026-10-03 09:14 UTC</div></div>
|
||||
<div class="datagrid-item"><div class="datagrid-title">Names</div><div class="datagrid-content font-monospace">mail.example.org</div></div>
|
||||
</div>
|
||||
<p class="text-secondary mt-3 mb-0">Renewal normally runs 30 days before expiry. Check port 80 and the <a href="#">system log</a>.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-lg-4">
|
||||
<div class="card">
|
||||
<div class="card-header"><h3 class="card-title">Milter sockets</h3><div class="card-actions"><span class="badge bg-green-lt">ok</span></div></div>
|
||||
<table class="table table-vcenter card-table">
|
||||
<thead><tr><th>Milter</th><th>State</th></tr></thead>
|
||||
<tbody>
|
||||
<tr><td>opendkim<div class="text-secondary small font-monospace">inet:127.0.0.1:8891</div></td><td><span class="badge bg-green-lt">ok</span></td></tr>
|
||||
<tr><td>journal-milter<div class="text-secondary small font-monospace">unix:/run/selfpost/journal.sock</div></td><td><span class="badge bg-green-lt">ok</span></td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-lg-4">
|
||||
<div class="card">
|
||||
<div class="card-header"><h3 class="card-title">Hostname and reverse DNS</h3><div class="card-actions"><span class="badge bg-green-lt">ok</span></div></div>
|
||||
<div class="card-body">
|
||||
<div class="datagrid">
|
||||
<div class="datagrid-item"><div class="datagrid-title">Hostname</div><div class="datagrid-content font-monospace">mail.example.org</div></div>
|
||||
<div class="datagrid-item"><div class="datagrid-title">Lookup</div><div class="datagrid-content font-monospace">→ 203.0.113.25<br>← mail.example.org</div></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-footer"><form method="post" action="#"><button type="submit" class="btn btn-sm"><i class="ti ti-refresh me-1"></i>Re-check DNS</button></form></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<footer class="footer footer-transparent">
|
||||
<div class="container-xl sp-foot">SelfPost 1.8.0 · © 2026 Mix · <a href="#">License (AGPL-3.0)</a> · <a href="#">Source</a> · No warranty</div>
|
||||
</footer>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,80 @@
|
||||
/* Direction B — "Console". Tabler (Bootstrap 5 admin kit) carries the shell,
|
||||
cards, tables, badges, input groups and the sign-in page as shipped; this
|
||||
file only re-points Tabler's variables at the brand and adds the two things
|
||||
Tabler has no word for: a sidebar group label and the DNS record block. */
|
||||
|
||||
:root, [data-bs-theme=light] {
|
||||
--tblr-font-sans-serif: var(--sp-sans);
|
||||
--tblr-font-monospace: var(--sp-mono);
|
||||
--tblr-body-bg: var(--sp-paper);
|
||||
--tblr-body-color: var(--sp-ink);
|
||||
--tblr-secondary: var(--sp-muted);
|
||||
--tblr-border-color: var(--sp-rule);
|
||||
--tblr-border-radius: 3px;
|
||||
--tblr-border-radius-lg: 4px;
|
||||
|
||||
--tblr-primary: #7a3b2e;
|
||||
--tblr-primary-rgb: 122, 59, 46;
|
||||
--tblr-link-color: #7a3b2e;
|
||||
--tblr-link-color-rgb: 122, 59, 46;
|
||||
--tblr-link-hover-color: #632f25;
|
||||
--tblr-link-hover-color-rgb: 99, 47, 37;
|
||||
|
||||
--tblr-green: #2c6b43; --tblr-green-rgb: 44, 107, 67;
|
||||
--tblr-yellow: #b5700f; --tblr-yellow-rgb: 181, 112, 15;
|
||||
--tblr-red: #b42318; --tblr-red-rgb: 180, 35, 24;
|
||||
--tblr-success: #2c6b43; --tblr-success-rgb: 44, 107, 67;
|
||||
--tblr-warning: #b5700f; --tblr-warning-rgb: 181, 112, 15;
|
||||
--tblr-danger: #b42318; --tblr-danger-rgb: 180, 35, 24;
|
||||
}
|
||||
body { font-feature-settings: normal; }
|
||||
.btn-primary { --tblr-btn-hover-bg: #632f25; --tblr-btn-active-bg: #632f25; }
|
||||
code, .font-monospace { font-family: var(--sp-mono); }
|
||||
|
||||
/* ---- sidebar: ink, with the stamp on its own paper */
|
||||
.navbar-vertical {
|
||||
--tblr-navbar-bg: var(--sp-ink); background: var(--sp-ink);
|
||||
--tblr-navbar-color: #aab2bd; --tblr-navbar-hover-color: #fff; --tblr-navbar-active-color: #fff;
|
||||
--tblr-navbar-border-color: transparent;
|
||||
}
|
||||
.navbar-vertical .navbar-brand { padding: 1.1rem 0 .9rem; justify-content: flex-start; }
|
||||
.navbar-vertical .navbar-brand img { height: 52px; width: auto; }
|
||||
.navbar-vertical .nav-link { padding-block: .42rem; }
|
||||
.navbar-vertical .nav-link-icon { font-size: 1.15rem; }
|
||||
.navbar-vertical .nav-item.active > .nav-link {
|
||||
background: rgb(255 255 255 / .07); box-shadow: inset 3px 0 0 #c4705e; color: #fff;
|
||||
}
|
||||
.sp-nav-label {
|
||||
font-family: var(--sp-mono); font-size: .66rem; font-weight: 600; letter-spacing: .14em;
|
||||
text-transform: uppercase; color: #6f7a88; padding: 1.1rem 1.5rem .3rem;
|
||||
}
|
||||
.sp-nav-foot { margin-top: auto; padding-bottom: .8rem; border-top: 1px solid rgb(255 255 255 / .08); padding-top: .6rem; }
|
||||
.sp-nav-user { color: #6f7a88; font-size: .8rem; padding: .3rem 1.5rem; }
|
||||
|
||||
/* ---- page */
|
||||
.page-pretitle { font-family: var(--sp-mono); letter-spacing: .1em; }
|
||||
.card-title { font-weight: 600; }
|
||||
.table thead th { font-family: var(--sp-mono); letter-spacing: .08em; }
|
||||
.sp-tile .card-body { display: flex; align-items: center; gap: .8rem; padding: .85rem 1rem; }
|
||||
.sp-tile .sp-tile-icon { font-size: 1.5rem; color: var(--sp-muted); }
|
||||
.sp-tile .fw-semibold { line-height: 1.2; }
|
||||
.sp-tile .status { margin-left: auto; }
|
||||
progress.sp-bar {
|
||||
appearance: none; -webkit-appearance: none; width: 8rem; height: .4rem; border: 0;
|
||||
border-radius: 1rem; overflow: hidden; vertical-align: middle; margin-right: .6rem; background: var(--sp-well);
|
||||
}
|
||||
progress.sp-bar::-webkit-progress-bar { background: var(--sp-well); }
|
||||
progress.sp-bar::-webkit-progress-value { background: var(--tblr-green); }
|
||||
progress.sp-bar::-moz-progress-bar { background: var(--tblr-green); }
|
||||
|
||||
/* ---- the DNS record block: Tabler's read-only input group, plus a status */
|
||||
.sp-record + .sp-record { border-top: 1px solid var(--sp-rule); }
|
||||
.sp-record .form-control[readonly], .sp-record textarea[readonly] {
|
||||
font-family: var(--sp-mono); font-size: .8rem; background: var(--sp-well);
|
||||
}
|
||||
.sp-record textarea { resize: none; }
|
||||
.sp-record .form-label { font-size: .75rem; color: var(--sp-muted); margin-bottom: .25rem; }
|
||||
|
||||
.sp-foot { color: var(--sp-muted); font-size: .8rem; }
|
||||
.sp-signin-mark { width: 15rem; height: auto; }
|
||||
.sp-tagline { font-family: var(--sp-mono); font-size: .72rem; letter-spacing: .16em; color: var(--sp-muted); }
|
||||
@@ -0,0 +1,200 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en" data-theme="light">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>example.org · Outbound domains · SelfPost</title>
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bulma@1.0.4/css/bulma.min.css">
|
||||
<link rel="stylesheet" href="../shared/brand.css">
|
||||
<link rel="stylesheet" href="theme.css">
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<nav class="navbar is-primary" aria-label="main navigation">
|
||||
<div class="container">
|
||||
<div class="navbar-brand">
|
||||
<a class="navbar-item" href="status.html"><img src="../../selfpost-stamp-compact.svg" alt="SelfPost"></a>
|
||||
</div>
|
||||
<div class="navbar-menu is-active">
|
||||
<div class="navbar-start">
|
||||
<a class="navbar-item" href="status.html">Overview</a>
|
||||
<div class="navbar-item has-dropdown is-hoverable sp-current">
|
||||
<a class="navbar-link" href="domain.html">Outbound</a>
|
||||
<div class="navbar-dropdown">
|
||||
<a class="navbar-item is-active" href="domain.html">Domains</a>
|
||||
<a class="navbar-item" href="#">Log</a>
|
||||
<a class="navbar-item" href="#">Queue</a>
|
||||
<a class="navbar-item" href="#">DMARC reports</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="navbar-item has-dropdown is-hoverable">
|
||||
<a class="navbar-link" href="#">Inbound</a>
|
||||
<div class="navbar-dropdown">
|
||||
<a class="navbar-item" href="#">Domains</a>
|
||||
<a class="navbar-item" href="#">Log</a>
|
||||
<a class="navbar-item" href="#">Queue</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="navbar-item has-dropdown is-hoverable">
|
||||
<a class="navbar-link" href="#">Server</a>
|
||||
<div class="navbar-dropdown">
|
||||
<a class="navbar-item" href="#">System log</a>
|
||||
<a class="navbar-item" href="#">Backup</a>
|
||||
<a class="navbar-item" href="#">Users</a>
|
||||
<a class="navbar-item" href="#">Settings</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="navbar-end">
|
||||
<div class="navbar-item has-dropdown is-hoverable">
|
||||
<a class="navbar-link" href="#">admin</a>
|
||||
<div class="navbar-dropdown is-right">
|
||||
<a class="navbar-item" href="#">Account</a>
|
||||
<a class="navbar-item" href="#">Help</a>
|
||||
<hr class="navbar-divider">
|
||||
<a class="navbar-item" href="login.html">Sign out</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<main>
|
||||
<div class="container sp-page">
|
||||
<div class="level mb-5">
|
||||
<div class="level-left">
|
||||
<div>
|
||||
<p class="sp-kicker"><a href="status.html">Outbound domains</a> /</p>
|
||||
<h1 class="title is-3 mb-0">example.org</h1>
|
||||
</div>
|
||||
</div>
|
||||
<div class="level-right">
|
||||
<div class="buttons">
|
||||
<form method="post" action="#"><button type="submit" class="button">Re-check DNS</button></form>
|
||||
<a class="button is-primary" href="#">Add application</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="columns">
|
||||
<div class="column is-3">
|
||||
<aside class="menu sp-side">
|
||||
<p class="menu-label">This domain</p>
|
||||
<ul class="menu-list">
|
||||
<li><a class="is-active" href="#dns">DNS records <span class="tag is-danger is-light">1</span></a></li>
|
||||
<li><a href="#apps">Applications <span class="tag is-light">2</span></a></li>
|
||||
<li><a href="#connection">Connection</a></li>
|
||||
</ul>
|
||||
<p class="menu-label">Rarely changed</p>
|
||||
<ul class="menu-list">
|
||||
<li><a href="#">DMARC reports</a></li>
|
||||
<li><a href="#">Rate limit <span class="tag is-success is-light">active</span></a></li>
|
||||
<li><a href="#">Export</a></li>
|
||||
<li><a class="has-text-danger" href="#">Delete domain</a></li>
|
||||
</ul>
|
||||
</aside>
|
||||
</div>
|
||||
|
||||
<div class="column is-9">
|
||||
<div class="box" id="dns">
|
||||
<div class="sp-box-head"><h2>DNS records</h2><div class="sp-end">Checked 3 min ago</div></div>
|
||||
|
||||
<div class="sp-record">
|
||||
<div class="sp-record-head"><h3>DKIM</h3><span class="tag is-success is-light">published</span><small>Not a secret · selector <code>sp2026</code></small></div>
|
||||
<div class="sp-row">
|
||||
<div class="field">
|
||||
<label class="label">Host / name</label>
|
||||
<div class="field has-addons"><div class="control is-expanded"><input class="input" readonly value="sp2026._domainkey.example.org"></div><div class="control"><button type="button" class="button">Copy</button></div></div>
|
||||
</div>
|
||||
<div class="field"><label class="label">Type</label><div class="control"><input class="input" readonly value="TXT"></div></div>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label class="label">Value to publish</label>
|
||||
<div class="field has-addons"><div class="control is-expanded"><textarea class="textarea" rows="3" readonly>v=DKIM1; k=rsa; p=MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAx3kqV0mJd8e2f1Yb7Qw5uT9sLr4nHc6pZaXo1vKe0yWm2gBt8RjN5dCq3hUf7lPs9aEi4oMz6xTb1kGv0nYw…IDAQAB</textarea></div><div class="control"><button type="button" class="button">Copy</button></div></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="sp-record">
|
||||
<div class="sp-record-head"><h3>SPF</h3><span class="tag is-danger is-light">mismatch</span><small>Merge into the existing record — never publish a second one</small></div>
|
||||
<div class="sp-row">
|
||||
<div class="field">
|
||||
<label class="label">Host / name</label>
|
||||
<div class="field has-addons"><div class="control is-expanded"><input class="input" readonly value="example.org"></div><div class="control"><button type="button" class="button">Copy</button></div></div>
|
||||
</div>
|
||||
<div class="field"><label class="label">Type</label><div class="control"><input class="input" readonly value="TXT"></div></div>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label class="label">Value to publish</label>
|
||||
<div class="field has-addons"><div class="control is-expanded"><input class="input" readonly value="v=spf1 ip4:203.0.113.25 ~all"></div><div class="control"><button type="button" class="button">Copy</button></div></div>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label class="label">In DNS now</label>
|
||||
<div class="control"><input class="input" readonly value="v=spf1 include:_spf.google.com ~all"></div>
|
||||
<p class="help is-danger">The published record does not authorize 203.0.113.25. Add <code>ip4:203.0.113.25</code> before <code>~all</code>.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="sp-record">
|
||||
<div class="sp-record-head"><h3>DMARC</h3><span class="tag is-success is-light">published</span><small>Report address from <a href="#">Settings</a></small></div>
|
||||
<div class="sp-row">
|
||||
<div class="field">
|
||||
<label class="label">Host / name</label>
|
||||
<div class="field has-addons"><div class="control is-expanded"><input class="input" readonly value="_dmarc.example.org"></div><div class="control"><button type="button" class="button">Copy</button></div></div>
|
||||
</div>
|
||||
<div class="field"><label class="label">Type</label><div class="control"><input class="input" readonly value="TXT"></div></div>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label class="label">Value to publish</label>
|
||||
<div class="field has-addons"><div class="control is-expanded"><input class="input" readonly value="v=DMARC1; p=none; rua=mailto:dmarc@mail.example.org"></div><div class="control"><button type="button" class="button">Copy</button></div></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="box" id="apps">
|
||||
<div class="sp-box-head"><h2>Applications</h2><div class="sp-end">SASL logins that may send as this domain</div></div>
|
||||
<table class="table is-fullwidth is-hoverable">
|
||||
<thead><tr><th>Login</th><th>May send as</th><th>30 days</th><th>Limits</th><th></th></tr></thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td class="sp-mono"><strong>prod-server</strong></td><td>*@example.org</td><td class="sp-muted">1 102 msg · peak 96/h</td>
|
||||
<td><span class="tag is-success is-light">200 / h</span></td>
|
||||
<td><div class="buttons are-small is-right is-flex-wrap-nowrap"><a class="button" href="#">Edit</a><a class="button" href="#">New password</a><a class="button is-danger is-light" href="#">Delete</a></div></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="sp-mono"><strong>alerts</strong></td><td>alerts@example.org, noc@example.org</td><td class="sp-muted">182 msg · peak 14/h</td>
|
||||
<td><span class="tag is-light">domain</span> <span class="tag is-success is-light">2 IPs</span></td>
|
||||
<td><div class="buttons are-small is-right is-flex-wrap-nowrap"><a class="button" href="#">Edit</a><a class="button" href="#">New password</a><a class="button is-danger is-light" href="#">Delete</a></div></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div class="columns">
|
||||
<div class="column">
|
||||
<div class="box" id="connection">
|
||||
<div class="sp-box-head"><h2>Connection</h2><div class="sp-end">Same for every domain</div></div>
|
||||
<p class="sp-mono">mail.example.org</p>
|
||||
<p class="sp-mono sp-muted">465 SSL/TLS · 587 STARTTLS</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="column">
|
||||
<div class="box">
|
||||
<div class="sp-box-head"><h2>Sent, 30 days</h2></div>
|
||||
<div class="sp-stats">
|
||||
<div><b>1 284</b><span class="sp-muted">messages</span></div>
|
||||
<div><b>96</b><span class="sp-muted">peak / h</span></div>
|
||||
<div><b>1.8</b><span class="sp-muted">average / h</span></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<footer class="sp-foot">SelfPost 1.8.0 · © 2026 Mix · <a href="#">License (AGPL-3.0)</a> · <a href="#">Source</a> · No warranty</footer>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,38 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en" data-theme="light">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>Sign in · SelfPost</title>
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bulma@1.0.4/css/bulma.min.css">
|
||||
<link rel="stylesheet" href="../shared/brand.css">
|
||||
<link rel="stylesheet" href="theme.css">
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<div class="columns is-gapless sp-signin">
|
||||
<div class="column is-half sp-signin-brand">
|
||||
<div>
|
||||
<img src="../../selfpost-stamp.svg" alt="SelfPost">
|
||||
<p>YOUR SMTP. YOUR RULES.</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="column is-half sp-signin-form">
|
||||
<form method="post" action="status.html">
|
||||
<h1 class="title is-3">Sign in</h1>
|
||||
<div class="field">
|
||||
<label class="label" for="username">Username</label>
|
||||
<div class="control"><input class="input" id="username" name="username" autocomplete="username" required></div>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label class="label" for="password">Password</label>
|
||||
<div class="control"><input class="input" id="password" name="password" type="password" autocomplete="current-password" required></div>
|
||||
</div>
|
||||
<div class="field mt-5"><button type="submit" class="button is-primary is-fullwidth">Sign in</button></div>
|
||||
</form>
|
||||
<p class="sp-foot">© 2026 Mix · <a href="#">License (AGPL-3.0)</a> · <a href="#">Source</a> · No warranty</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,117 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en" data-theme="light">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>Overview · SelfPost</title>
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bulma@1.0.4/css/bulma.min.css">
|
||||
<link rel="stylesheet" href="../shared/brand.css">
|
||||
<link rel="stylesheet" href="theme.css">
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<nav class="navbar is-primary" aria-label="main navigation">
|
||||
<div class="container">
|
||||
<div class="navbar-brand">
|
||||
<a class="navbar-item" href="status.html"><img src="../../selfpost-stamp-compact.svg" alt="SelfPost"></a>
|
||||
</div>
|
||||
<div class="navbar-menu is-active">
|
||||
<div class="navbar-start">
|
||||
<a class="navbar-item is-active" href="status.html">Overview</a>
|
||||
<div class="navbar-item has-dropdown is-hoverable">
|
||||
<a class="navbar-link" href="domain.html">Outbound</a>
|
||||
<div class="navbar-dropdown">
|
||||
<a class="navbar-item" href="domain.html">Domains</a>
|
||||
<a class="navbar-item" href="#">Log</a>
|
||||
<a class="navbar-item" href="#">Queue</a>
|
||||
<a class="navbar-item" href="#">DMARC reports</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="navbar-item has-dropdown is-hoverable">
|
||||
<a class="navbar-link" href="#">Inbound</a>
|
||||
<div class="navbar-dropdown">
|
||||
<a class="navbar-item" href="#">Domains</a>
|
||||
<a class="navbar-item" href="#">Log</a>
|
||||
<a class="navbar-item" href="#">Queue</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="navbar-item has-dropdown is-hoverable">
|
||||
<a class="navbar-link" href="#">Server</a>
|
||||
<div class="navbar-dropdown">
|
||||
<a class="navbar-item" href="#">System log</a>
|
||||
<a class="navbar-item" href="#">Backup</a>
|
||||
<a class="navbar-item" href="#">Users</a>
|
||||
<a class="navbar-item" href="#">Settings</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="navbar-end">
|
||||
<div class="navbar-item has-dropdown is-hoverable">
|
||||
<a class="navbar-link" href="#">admin</a>
|
||||
<div class="navbar-dropdown is-right">
|
||||
<a class="navbar-item" href="#">Account</a>
|
||||
<a class="navbar-item" href="#">Help</a>
|
||||
<hr class="navbar-divider">
|
||||
<a class="navbar-item" href="login.html">Sign out</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<main>
|
||||
<div class="container sp-page">
|
||||
<div class="level mb-5">
|
||||
<div class="level-left">
|
||||
<div>
|
||||
<p class="sp-kicker">mail.example.org</p>
|
||||
<h1 class="title is-3 mb-0">Overview</h1>
|
||||
</div>
|
||||
</div>
|
||||
<div class="level-right sp-muted is-size-7">Updated 14:02:31 UTC · refreshes every 10 s</div>
|
||||
</div>
|
||||
|
||||
<div class="box sp-health">
|
||||
<a href="#"><div class="sp-h-name">Machine</div><div class="sp-h-value">12 % CPU</div><div class="sp-h-sub">41 % memory</div></a>
|
||||
<a href="#"><div class="sp-h-name">Processes</div><div class="sp-h-value">5 of 5</div><div class="sp-h-sub">running</div></a>
|
||||
<a href="#"><div class="sp-h-name">Queues</div><div class="sp-h-value">Empty</div><div class="sp-h-sub">outbound and inbound</div></a>
|
||||
<a href="#" class="warn"><div class="sp-h-name">TLS certificate</div><div class="sp-h-value">12 days</div><div class="sp-h-sub">renewal has not run</div></a>
|
||||
<a href="#"><div class="sp-h-name">Milter sockets</div><div class="sp-h-value">2 of 2</div><div class="sp-h-sub">answering</div></a>
|
||||
<a href="#"><div class="sp-h-name">Reverse DNS</div><div class="sp-h-value">Match</div><div class="sp-h-sub">203.0.113.25</div></a>
|
||||
</div>
|
||||
|
||||
<div class="columns">
|
||||
<div class="column is-7">
|
||||
<div class="box">
|
||||
<div class="sp-box-head"><h2>Outbound domains</h2><div class="sp-end"><a class="button is-small is-primary" href="#">Add domain</a></div></div>
|
||||
<table class="table is-fullwidth is-hoverable">
|
||||
<thead><tr><th>Domain</th><th>DNS</th><th>Apps</th><th>30 days</th></tr></thead>
|
||||
<tbody>
|
||||
<tr><td><a href="domain.html"><strong>example.org</strong></a></td><td><span class="tag is-success is-light">DKIM</span> <span class="tag is-danger is-light">SPF</span> <span class="tag is-success is-light">DMARC</span></td><td>2</td><td class="sp-muted">1 284 msg</td></tr>
|
||||
<tr><td><a href="#"><strong>shop.example.org</strong></a></td><td><span class="tag is-success is-light">DKIM</span> <span class="tag is-success is-light">SPF</span> <span class="tag is-success is-light">DMARC</span></td><td>1</td><td class="sp-muted">18 920 msg</td></tr>
|
||||
<tr><td><a href="#"><strong>notify.acme.io</strong></a></td><td><span class="tag is-success is-light">DKIM</span> <span class="tag is-success is-light">SPF</span> <span class="tag is-warning is-light">DMARC</span></td><td>3</td><td class="sp-muted">402 msg</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
<div class="column is-5">
|
||||
<div class="box">
|
||||
<div class="sp-box-head"><h2>Inbound domains</h2><div class="sp-end"><a class="button is-small" href="#">Add domain</a></div></div>
|
||||
<table class="table is-fullwidth is-hoverable">
|
||||
<thead><tr><th>Domain</th><th>MX</th><th>Upstream</th></tr></thead>
|
||||
<tbody>
|
||||
<tr><td><a href="#"><strong>lists.example.org</strong></a></td><td><span class="tag is-success is-light">MX</span></td><td class="sp-muted sp-mono is-size-7">mx.internal:25</td></tr>
|
||||
<tr><td><a href="#"><strong>acme.io</strong></a></td><td><span class="tag is-success is-light">MX</span></td><td class="sp-muted sp-mono is-size-7">10.0.4.12:2525</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<footer class="sp-foot">SelfPost 1.8.0 · © 2026 Mix · <a href="#">License (AGPL-3.0)</a> · <a href="#">Source</a> · No warranty</footer>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,98 @@
|
||||
/* Direction C — "Counter". Bulma v1 (pure CSS, no JavaScript, no data: URIs)
|
||||
carries the navbar with hover dropdowns, columns, boxes, tables, tags,
|
||||
the side menu and input-with-addon fields. This file points Bulma's HSL
|
||||
variables at the brand and adds the health strip and the record block. */
|
||||
|
||||
:root {
|
||||
--bulma-family-primary: var(--sp-sans);
|
||||
--bulma-family-secondary: var(--sp-sans);
|
||||
--bulma-family-code: var(--sp-mono);
|
||||
--bulma-primary-h: 10deg; --bulma-primary-s: 45%; --bulma-primary-l: 33%;
|
||||
--bulma-link-h: 10deg; --bulma-link-s: 45%; --bulma-link-l: 33%;
|
||||
--bulma-success-h: 138deg; --bulma-success-s: 42%; --bulma-success-l: 30%;
|
||||
--bulma-warning-h: 36deg; --bulma-warning-s: 85%; --bulma-warning-l: 55%;
|
||||
--bulma-danger-h: 4deg; --bulma-danger-s: 76%; --bulma-danger-l: 40%;
|
||||
--bulma-radius: 6px; --bulma-radius-large: 10px;
|
||||
--bulma-body-size: 15px;
|
||||
/* Bulma derives link text and the on-primary colour from lightness steps;
|
||||
at the brick's 33 % both land off-brand, so they are pinned. */
|
||||
--bulma-link-text: var(--sp-brick);
|
||||
--bulma-link-text-hover: var(--sp-brick-dark);
|
||||
}
|
||||
.button.is-primary, .button.is-primary:hover { color: #fff; }
|
||||
a strong { color: inherit; }
|
||||
html { background: var(--sp-paper); }
|
||||
body { background: var(--sp-paper); color: var(--sp-ink); min-height: 100vh; display: flex; flex-direction: column; }
|
||||
main { flex: 1; }
|
||||
code { background: var(--sp-well); color: var(--sp-ink); border-radius: 3px; padding: .1em .35em; font-size: .85em; }
|
||||
|
||||
/* ---- navbar: brick, the stamp on it, hover dropdowns */
|
||||
.navbar.is-primary { --bulma-navbar-height: 4rem; }
|
||||
.navbar-brand img { max-height: 3rem; height: 3rem; }
|
||||
.navbar.is-primary .navbar-item, .navbar.is-primary .navbar-link { color: var(--sp-on-brick); font-weight: 500; }
|
||||
.navbar.is-primary .navbar-link::after { border-color: var(--sp-on-brick); }
|
||||
.navbar.is-primary .navbar-start > .navbar-item.is-active,
|
||||
.navbar.is-primary .navbar-start > .has-dropdown.sp-current > .navbar-link { box-shadow: inset 0 -3px 0 var(--sp-on-brick); }
|
||||
.navbar-dropdown { border-top: 0; box-shadow: 0 8px 24px rgb(18 22 28 / .14); }
|
||||
.navbar.is-primary .navbar-dropdown .navbar-item { color: var(--sp-ink); font-weight: 400; }
|
||||
.navbar.is-primary .navbar-dropdown .navbar-item.is-active { color: var(--sp-brick); font-weight: 600; background: var(--sp-brick-tint); }
|
||||
|
||||
/* ---- page */
|
||||
.sp-page { padding-block: 2rem 2.5rem; }
|
||||
.sp-kicker { font-family: var(--sp-mono); font-size: .75rem; letter-spacing: .1em; text-transform: uppercase; color: var(--sp-muted); }
|
||||
.sp-kicker a { color: inherit; }
|
||||
.title { color: var(--sp-ink); font-weight: 600; }
|
||||
.box { box-shadow: none; border: 1px solid var(--sp-rule); }
|
||||
.box > .sp-box-head { display: flex; align-items: center; gap: .75rem; margin-bottom: 1rem; }
|
||||
.box > .sp-box-head h2 { font-size: 1.1rem; font-weight: 600; margin: 0; }
|
||||
.box > .sp-box-head .sp-end { margin-left: auto; color: var(--sp-muted); font-size: .85rem; display: flex; gap: .6rem; align-items: center; }
|
||||
.table { background: transparent; }
|
||||
.table thead th { font-family: var(--sp-mono); font-size: .7rem; font-weight: 400; letter-spacing: .08em; text-transform: uppercase; color: var(--sp-muted); }
|
||||
.table td { vertical-align: middle; }
|
||||
.tag { font-family: var(--sp-mono); font-weight: 600; letter-spacing: .04em; }
|
||||
.sp-muted { color: var(--sp-muted); }
|
||||
.sp-mono { font-family: var(--sp-mono); }
|
||||
|
||||
/* ---- health strip */
|
||||
.sp-health { display: grid; grid-template-columns: repeat(auto-fit, minmax(9.5rem, 1fr)); gap: 0; padding: 0; overflow: hidden; }
|
||||
.sp-health > a {
|
||||
display: block; padding: 1rem 1.2rem; color: var(--sp-ink); border-right: 1px solid var(--sp-rule);
|
||||
box-shadow: inset 0 3px 0 var(--sp-ok);
|
||||
}
|
||||
.sp-health > a:last-child { border-right: 0; }
|
||||
.sp-health > a:hover { background: var(--sp-paper); }
|
||||
.sp-health > a.warn { box-shadow: inset 0 3px 0 #d9962b; background: var(--sp-warn-bg); }
|
||||
.sp-health > a.fail { box-shadow: inset 0 3px 0 var(--sp-fail); background: var(--sp-fail-bg); }
|
||||
.sp-health .sp-h-name { font-family: var(--sp-mono); font-size: .68rem; letter-spacing: .08em; text-transform: uppercase; color: var(--sp-muted); }
|
||||
.sp-health .sp-h-value { font-size: 1.15rem; font-weight: 600; line-height: 1.3; }
|
||||
.sp-health .sp-h-sub { font-size: .8rem; color: var(--sp-muted); }
|
||||
|
||||
/* ---- domain page */
|
||||
.sp-side { position: sticky; top: 1.5rem; }
|
||||
.menu-label { font-family: var(--sp-mono); letter-spacing: .1em; }
|
||||
.menu-list a { display: flex; align-items: center; gap: .5rem; }
|
||||
.menu-list a .tag { margin-left: auto; }
|
||||
.sp-record + .sp-record { border-top: 1px solid var(--sp-rule); margin-top: 1.25rem; padding-top: 1.25rem; }
|
||||
.sp-record-head { display: flex; align-items: center; gap: .6rem; margin-bottom: .75rem; }
|
||||
.sp-record-head h3 { font-size: 1rem; font-weight: 600; margin: 0; }
|
||||
.sp-record-head small { margin-left: auto; color: var(--sp-muted); }
|
||||
.sp-record .label { font-size: .75rem; font-weight: 400; color: var(--sp-muted); margin-bottom: .25rem; }
|
||||
.sp-record .input[readonly], .sp-record .textarea[readonly] {
|
||||
font-family: var(--sp-mono); font-size: .82rem; background: var(--sp-well); box-shadow: none;
|
||||
}
|
||||
.sp-record .textarea { resize: none; min-height: 0; }
|
||||
.sp-record .field:not(:last-child) { margin-bottom: .6rem; }
|
||||
.sp-row { display: grid; grid-template-columns: 1fr 7rem; gap: .75rem; }
|
||||
.sp-stats { display: grid; grid-template-columns: repeat(3, 1fr); gap: 1rem; }
|
||||
.sp-stats b { display: block; font-size: 1.5rem; font-weight: 600; line-height: 1.2; }
|
||||
|
||||
/* ---- sign-in */
|
||||
.sp-signin { min-height: 100vh; margin: 0; }
|
||||
.sp-signin-brand { background: var(--sp-brick); display: grid; place-content: center; text-align: center; padding: 3rem; }
|
||||
.sp-signin-brand img { width: 20rem; max-width: 100%; height: auto; }
|
||||
.sp-signin-brand p { font-family: var(--sp-mono); letter-spacing: .18em; font-size: .8rem; color: var(--sp-on-brick); margin-top: 1.6rem; }
|
||||
.sp-signin-form { display: flex; flex-direction: column; justify-content: center; padding: 3rem; }
|
||||
.sp-signin-form form { width: 21rem; max-width: 100%; margin-inline: auto; }
|
||||
.sp-foot { color: var(--sp-muted); font-size: .8rem; padding-block: 1.2rem; }
|
||||
.sp-foot a { color: inherit; text-decoration: underline; }
|
||||
.sp-signin-form .sp-foot { text-align: center; margin-top: 2.5rem; }
|
||||
|
After Width: | Height: | Size: 58 KiB |
|
After Width: | Height: | Size: 90 KiB |
|
After Width: | Height: | Size: 418 KiB |
|
After Width: | Height: | Size: 79 KiB |
|
After Width: | Height: | Size: 70 KiB |
|
After Width: | Height: | Size: 80 KiB |
|
After Width: | Height: | Size: 124 KiB |
|
After Width: | Height: | Size: 600 KiB |
|
After Width: | Height: | Size: 58 KiB |
|
After Width: | Height: | Size: 123 KiB |
|
After Width: | Height: | Size: 74 KiB |
|
After Width: | Height: | Size: 43 KiB |
|
After Width: | Height: | Size: 41 KiB |
|
After Width: | Height: | Size: 66 KiB |
|
After Width: | Height: | Size: 86 KiB |
|
After Width: | Height: | Size: 78 KiB |
|
After Width: | Height: | Size: 146 KiB |
|
After Width: | Height: | Size: 197 KiB |
|
After Width: | Height: | Size: 88 KiB |
|
After Width: | Height: | Size: 102 KiB |
|
After Width: | Height: | Size: 122 KiB |
|
After Width: | Height: | Size: 71 KiB |
|
After Width: | Height: | Size: 95 KiB |
|
After Width: | Height: | Size: 70 KiB |
|
After Width: | Height: | Size: 54 KiB |
|
After Width: | Height: | Size: 103 KiB |
|
After Width: | Height: | Size: 58 KiB |
|
After Width: | Height: | Size: 98 KiB |
|
After Width: | Height: | Size: 68 KiB |
@@ -0,0 +1,22 @@
|
||||
/* Cover sheet for the three directions. No library on purpose: it must not
|
||||
look like any one of them. */
|
||||
body { margin: 0; background: var(--sp-paper); color: var(--sp-ink); font: 16px/1.55 var(--sp-sans); }
|
||||
main { max-width: 68rem; margin: 0 auto; padding: 3rem 1.5rem 4rem; }
|
||||
h1 { font-size: 2rem; font-weight: 600; margin: .2rem 0 1rem; text-wrap: balance; }
|
||||
h2 { font-size: 1.15rem; font-weight: 600; margin: 2.6rem 0 .8rem; }
|
||||
h3 { font-size: 1.4rem; font-weight: 600; margin: .1rem 0 .6rem; }
|
||||
.kicker { font-family: var(--sp-mono); font-size: .75rem; letter-spacing: .1em; text-transform: uppercase; color: var(--sp-brick); margin: 0; }
|
||||
.lead { max-width: 46rem; color: var(--sp-ink-2); }
|
||||
ul { padding-left: 1.2rem; max-width: 52rem; }
|
||||
li { margin-bottom: .5rem; }
|
||||
a { color: var(--sp-brick); }
|
||||
code { font-family: var(--sp-mono); font-size: .85em; background: var(--sp-well); padding: .1em .35em; border-radius: 3px; }
|
||||
.cards { display: grid; grid-template-columns: repeat(auto-fit, minmax(18rem, 1fr)); gap: 1.2rem; }
|
||||
.cards section { background: var(--sp-sheet); border: 1px solid var(--sp-rule); border-top: 3px solid var(--sp-brick); padding: 1.3rem 1.4rem; display: flex; flex-direction: column; }
|
||||
.cards p { margin: 0 0 1rem; }
|
||||
.links { margin-top: auto !important; margin-bottom: 0 !important; display: flex; gap: 1.1rem; font-weight: 600; }
|
||||
.scroll { overflow-x: auto; }
|
||||
table { border-collapse: collapse; width: 100%; background: var(--sp-sheet); border: 1px solid var(--sp-rule); font-size: .93rem; }
|
||||
th, td { text-align: left; vertical-align: top; padding: .6rem .9rem; border-bottom: 1px solid var(--sp-rule); }
|
||||
thead th { font-family: var(--sp-mono); font-size: .72rem; letter-spacing: .08em; text-transform: uppercase; color: var(--sp-muted); font-weight: 400; }
|
||||
tbody th { font-weight: 600; white-space: nowrap; }
|
||||
@@ -0,0 +1,138 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>Panel redesign · three directions</title>
|
||||
<link rel="stylesheet" href="shared/brand.css">
|
||||
<link rel="stylesheet" href="index.css">
|
||||
</head>
|
||||
<body>
|
||||
<main>
|
||||
<p class="kicker">SelfPost · panel redesign · stage 2</p>
|
||||
<h1>The accepted direction, every screen</h1>
|
||||
<p class="lead">Direction <strong>C</strong> (Bulma) was picked as the base: the important things in view, details one
|
||||
or two clicks away. It takes the postal signature of <strong>A</strong> — the postmark verdict, numbered mono section
|
||||
heads, the stamp's perforated paper edge under a brick bar that carries the wordmark — and the icons of <strong>B</strong>. Thirty-four screens and a components page live in
|
||||
<a href="panel/overview.html">panel/</a>. Serve this folder from the repository root: the pages load the panel's own
|
||||
fonts and mark by relative path.</p>
|
||||
|
||||
<h2>Screen map</h2>
|
||||
<div class="scroll">
|
||||
<table>
|
||||
<tbody>
|
||||
<tr><th>Signed out</th><td><a href="panel/login.html">Sign in</a> · <a href="panel/login-no-admin.html">sign in before an administrator exists</a> · <a href="panel/setup.html">Create administrator</a></td></tr>
|
||||
<tr><th>Overview</th><td><a href="panel/overview.html">Overview</a> — postmark verdict, six health cards, both domain lists</td></tr>
|
||||
<tr><th>Outbound</th><td><a href="panel/out-domains.html">Domains</a> · <a href="panel/out-domain.html">domain</a> ·
|
||||
<a href="panel/out-domain-settings.html">domain settings</a> · <a href="panel/out-app.html">application form</a> ·
|
||||
<a href="panel/out-app-created.html">password shown once</a> · <a href="panel/out-domain-delete.html">delete domain</a><br>
|
||||
<a href="panel/out-log.html">Log</a> · <a href="panel/out-message.html">message</a> ·
|
||||
<a href="panel/out-queue.html">Queue</a> · <a href="panel/dmarc.html">DMARC reports</a> ·
|
||||
<a href="panel/dmarc-domain.html">by domain</a> · <a href="panel/dmarc-report.html">one report</a></td></tr>
|
||||
<tr><th>Inbound</th><td><a href="panel/in-domains.html">Domains</a> · <a href="panel/in-domain.html">domain</a> ·
|
||||
<a href="panel/in-domain-delete.html">delete domain</a> · <a href="panel/in-log.html">Log</a> ·
|
||||
<a href="panel/in-queue.html">Queue</a> (empty state)<br>
|
||||
<a href="panel/in-quarantine.html">Quarantine</a> · <a href="panel/in-quarantine-message.html">held message</a> ·
|
||||
<a href="panel/in-filter-lists.html">Filter lists</a> — roadmap items, drawn as their target look</td></tr>
|
||||
<tr><th>Server</th><td><a href="panel/health.html">Health</a> · <a href="panel/preflight.html">Preflight</a> (roadmap) · <a href="panel/system-log.html">System log</a> ·
|
||||
<a href="panel/backup.html">Backup</a> · <a href="panel/users.html">Users</a> · <a href="panel/user.html">user form</a> ·
|
||||
<a href="panel/user-delete.html">delete user</a> · <a href="panel/settings.html">Settings</a></td></tr>
|
||||
<tr><th>User menu</th><td><a href="panel/account.html">Account</a> · <a href="panel/help.html">Help</a></td></tr>
|
||||
<tr><th>Contract</th><td><a href="panel/components.html">Components</a> — every building block in every state ·
|
||||
<a href="../../plans/panel-redesign.md">the plan</a> · <code>panel/check.py</code> lints the mockups and writes <code>panel/outlines.json</code></td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<h2>What changed against today's panel</h2>
|
||||
<ul>
|
||||
<li><strong>Overview shows verdicts, Health shows tables.</strong> Process lists, socket paths and interface counters moved to
|
||||
<em>Server › Health</em>, one click from each card.</li>
|
||||
<li><strong>Every page is one of three layouts</strong>: a list (form on top, table below), a detail page (sticky side menu +
|
||||
numbered boxes), or a form page (boxes in equal columns). No page picks its own width.</li>
|
||||
<li><strong>Own password is Account</strong>, instance defaults are Settings. Export sits with the domain, import with Backup,
|
||||
and each links to the other.</li>
|
||||
<li><strong>Queues are tables</strong>, not a <code>postqueue -p</code> dump — this needs <code>postqueue -j</code> parsing.
|
||||
The inbound queue is the same queue filtered by recipient domain.</li>
|
||||
<li><strong>Confirm pages say what will be lost</strong> as a list, beside a hint for the usual alternative (export before delete).</li>
|
||||
<li>Own components on top of Bulma: postmark, perforated edge, numbered box head, health card, DNS record block, facts grid,
|
||||
timeline, log pane. The mockup's <code>theme.css</code> is about 200 lines.</li>
|
||||
<li><code>panel/build.py</code> wraps the fragments in <code>panel/src/</code> with the shared navbar; the built pages are committed,
|
||||
so nothing has to be run to view them.</li>
|
||||
</ul>
|
||||
|
||||
<h2>Stage 1 · three directions on three key screens</h2>
|
||||
<p class="lead">Kept for the record: Overview, an outbound domain and sign-in, drawn three ways.</p>
|
||||
|
||||
<h2>Shared by all three</h2>
|
||||
<ul>
|
||||
<li><strong>Navigation by mail direction.</strong> Overview · <em>Outbound</em> (Domains, Log, Queue, DMARC reports) ·
|
||||
<em>Inbound</em> (Domains, Log, Queue) · <em>Server</em> (System log, Backup, Users, Settings). Account, Help and
|
||||
Sign out live with the user name. Eleven flat entries become four groups.</li>
|
||||
<li><strong>One block per DNS record.</strong> Today "DNS status" and "records to publish" show every host twice.
|
||||
Here a record is one block: its status, what to publish, and — only on a mismatch — what DNS holds now.</li>
|
||||
<li><strong>The domain page stops being nine cards.</strong> DNS and applications are the page; limits, report address,
|
||||
export and delete are "rarely changed" and sit behind one click.</li>
|
||||
<li><strong>Applications are a table</strong>; editing one is its own form page instead of a three-column panel inside a list row.</li>
|
||||
<li>Brand carriers stay: the stamp, brick, IBM Plex. No inline <code>style</code> or script in any mockup.</li>
|
||||
</ul>
|
||||
|
||||
<h2>The directions</h2>
|
||||
<div class="cards">
|
||||
<section>
|
||||
<p class="kicker">A · Pico CSS 2.1</p>
|
||||
<h3>Blank</h3>
|
||||
<p>The panel as a postal form: one centred sheet, numbered sections, mono field labels, a postmark for the
|
||||
verdict. Overview is a ledger — one row per check, the row opens into its detail. Sections on the first
|
||||
header row, the section's pages on the second.</p>
|
||||
<p class="links"><a href="a/status.html">Overview</a><a href="a/domain.html">Domain</a><a href="a/login.html">Sign in</a></p>
|
||||
</section>
|
||||
<section>
|
||||
<p class="kicker">B · Tabler 1.5 (Bootstrap 5)</p>
|
||||
<h3>Console</h3>
|
||||
<p>The classic admin kit: ink sidebar with every page one click away, status tiles, cards and tables in a
|
||||
12-column grid, read-only input groups with Copy. Almost nothing is ours; it also looks the most like
|
||||
every other admin panel.</p>
|
||||
<p class="links"><a href="b/status.html">Overview</a><a href="b/domain.html">Domain</a><a href="b/login.html">Sign in</a></p>
|
||||
</section>
|
||||
<section>
|
||||
<p class="kicker">C · Bulma 1.0</p>
|
||||
<h3>Counter</h3>
|
||||
<p>Brick top bar with CSS-only hover menus. Overview is the whole service on one screen: a health strip over
|
||||
the outbound and inbound domain lists. The domain page gets a sticky side menu. Softer, rounder, the
|
||||
boldest use of the brand colour.</p>
|
||||
<p class="links"><a href="c/status.html">Overview</a><a href="c/domain.html">Domain</a><a href="c/login.html">Sign in</a></p>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<h2>What each one costs</h2>
|
||||
<div class="scroll">
|
||||
<table>
|
||||
<thead><tr><th></th><th>A · Pico</th><th>B · Tabler</th><th>C · Bulma</th></tr></thead>
|
||||
<tbody>
|
||||
<tr><th>Licence</th><td>MIT</td><td>MIT</td><td>MIT</td></tr>
|
||||
<tr><th>Vendored CSS (minified)</th><td>83 KB</td><td>694 KB</td><td>678 KB</td></tr>
|
||||
<tr><th>JavaScript</th><td>none</td><td>none for these screens; Bootstrap bundle only if dropdowns / modals are wanted</td><td>none</td></tr>
|
||||
<tr><th>Build step</th><td>none</td><td>none</td><td>none (a Sass build could trim it; not needed)</td></tr>
|
||||
<tr><th>CSP <code>default-src 'self'</code></th>
|
||||
<td>needs <code>img-src 'self' data:</code> — 14 <code>data:</code> SVGs (select chevron, checkbox tick)</td>
|
||||
<td>needs <code>img-src 'self' data:</code> — 28 <code>data:</code> SVGs</td>
|
||||
<td>works as is — no <code>data:</code> URIs</td></tr>
|
||||
<tr><th>Our own CSS in the mockup</th><td>≈ 200 lines</td><td>≈ 80 lines</td><td>≈ 100 lines</td></tr>
|
||||
<tr><th>Markup style</th><td>semantic HTML, few classes — closest to today's templates</td><td>class-heavy Bootstrap markup</td><td>class-based, moderate</td></tr>
|
||||
<tr><th>Own components</th><td>sheet, ledger row, postmark, record block</td><td>sidebar group label, record block</td><td>health strip, record block</td></tr>
|
||||
<tr><th>Recognisable</th><td>most</td><td>least</td><td>middle</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<h2>Open points</h2>
|
||||
<ul>
|
||||
<li>Icons in B come from Tabler Icons (MIT) through its webfont for the mockup; the panel would vendor only the used glyphs as an SVG sprite.</li>
|
||||
<li><em>Inbound log</em> rides on the agreed <code>inbound-antispam-panel</code> journal; <em>Inbound queue</em> is a new filter over the one Postfix queue and needs its own roadmap row.</li>
|
||||
<li>Dark scheme and the narrow-screen shell are not drawn at this stage; all three libraries ship both.</li>
|
||||
<li>The earlier <a href="../panel-ui/index.html">panel-ui</a> experiment and <a href="../../plans/panel-redesign.md">panel-redesign.md</a> are superseded by whichever direction is accepted.</li>
|
||||
</ul>
|
||||
</main>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,113 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en" data-theme="light">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>Account · SelfPost</title>
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bulma@1.0.4/css/bulma.min.css">
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@tabler/icons-webfont@3/dist/tabler-icons.min.css">
|
||||
<link rel="stylesheet" href="../shared/brand.css">
|
||||
<link rel="stylesheet" href="theme.css">
|
||||
</head>
|
||||
<body>
|
||||
<nav class="navbar is-primary" aria-label="main navigation">
|
||||
<div class="container">
|
||||
<div class="navbar-brand"><a class="navbar-item" href="overview.html"><img src="../../selfpost-wordmark.svg" alt="SelfPost"></a></div>
|
||||
<div class="navbar-menu is-active">
|
||||
<div class="navbar-start">
|
||||
<a class="navbar-item" href="overview.html">Overview</a>
|
||||
<div class="navbar-item has-dropdown is-hoverable">
|
||||
<a class="navbar-link" href="out-domains.html">Outbound</a>
|
||||
<div class="navbar-dropdown">
|
||||
<a class="navbar-item" href="out-domains.html"><i class="ti ti-world-upload"></i>Domains</a>
|
||||
<a class="navbar-item" href="out-log.html"><i class="ti ti-list-details"></i>Log</a>
|
||||
<a class="navbar-item" href="out-queue.html"><i class="ti ti-stack-2"></i>Queue</a>
|
||||
<a class="navbar-item" href="dmarc.html"><i class="ti ti-report-analytics"></i>DMARC reports</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="navbar-item has-dropdown is-hoverable">
|
||||
<a class="navbar-link" href="in-domains.html">Inbound</a>
|
||||
<div class="navbar-dropdown">
|
||||
<a class="navbar-item" href="in-domains.html"><i class="ti ti-world-download"></i>Domains</a>
|
||||
<a class="navbar-item" href="in-log.html"><i class="ti ti-list-details"></i>Log</a>
|
||||
<a class="navbar-item" href="in-queue.html"><i class="ti ti-stack-2"></i>Queue</a>
|
||||
<a class="navbar-item" href="in-quarantine.html"><i class="ti ti-mail-pause"></i>Quarantine</a>
|
||||
<a class="navbar-item" href="in-filter-lists.html"><i class="ti ti-filter"></i>Filter lists</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="navbar-item has-dropdown is-hoverable">
|
||||
<a class="navbar-link" href="health.html">Server</a>
|
||||
<div class="navbar-dropdown">
|
||||
<a class="navbar-item" href="health.html"><i class="ti ti-heartbeat"></i>Health</a>
|
||||
<a class="navbar-item" href="preflight.html"><i class="ti ti-checklist"></i>Preflight</a>
|
||||
<a class="navbar-item" href="system-log.html"><i class="ti ti-file-text"></i>System log</a>
|
||||
<a class="navbar-item" href="backup.html"><i class="ti ti-archive"></i>Backup</a>
|
||||
<a class="navbar-item" href="users.html"><i class="ti ti-users"></i>Users</a>
|
||||
<a class="navbar-item" href="settings.html"><i class="ti ti-settings"></i>Settings</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="navbar-end">
|
||||
<div class="navbar-item has-dropdown is-hoverable sp-current">
|
||||
<a class="navbar-link" href="account.html"><i class="ti ti-user-circle"></i>admin</a>
|
||||
<div class="navbar-dropdown is-right">
|
||||
<a class="navbar-item" href="account.html"><i class="ti ti-key"></i>Account</a>
|
||||
<a class="navbar-item" href="help.html"><i class="ti ti-help"></i>Help</a>
|
||||
<hr class="navbar-divider">
|
||||
<form method="post" action="login.html"><button type="submit" class="navbar-item"><i class="ti ti-logout"></i>Sign out</button></form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
<div class="sp-perf" aria-hidden="true"></div>
|
||||
|
||||
<main>
|
||||
<div class="container sp-page">
|
||||
<div class="sp-head">
|
||||
<div class="sp-grow">
|
||||
<p class="sp-kicker">Signed in as admin · global</p>
|
||||
<h1 class="title is-3">Account</h1>
|
||||
<p class="sp-lead">Who you are to this panel. Applications keep their own logins and passwords and are not affected.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="columns">
|
||||
<div class="column is-6">
|
||||
<div class="box">
|
||||
<div class="sp-box-head"><span class="sp-no">01</span><h2>Profile</h2></div>
|
||||
<div class="sp-box-body">
|
||||
<form class="sp-form" method="post" action="#">
|
||||
<div class="field"><label class="label" for="username">Username</label><div class="control has-icons-left"><input class="input" id="username" name="username" value="admin" autocomplete="username" spellcheck="false" required><span class="icon is-left"><i class="ti ti-user"></i></span></div></div>
|
||||
<div class="field"><label class="label" for="email">E-mail</label><div class="control has-icons-left"><input class="input" id="email" name="email" type="email" value="mix@example.org" autocomplete="email"><span class="icon is-left"><i class="ti ti-mail"></i></span></div>
|
||||
<p class="help">Your own address. A domain's report address can be filled with it from Domain settings.</p></div>
|
||||
<button type="submit" class="button is-primary">Save profile</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="column is-6">
|
||||
<div class="box">
|
||||
<div class="sp-box-head"><span class="sp-no">02</span><h2>Password</h2></div>
|
||||
<div class="sp-box-body">
|
||||
<form class="sp-form" method="post" action="#">
|
||||
<div class="field"><label class="label" for="cur">Current password</label><div class="control"><input class="input" id="cur" name="current_password" type="password" autocomplete="current-password" required></div></div>
|
||||
<div class="sp-pair">
|
||||
<div class="field"><label class="label" for="new">New password</label><div class="control"><input class="input" id="new" name="new_password" type="password" autocomplete="new-password" required></div></div>
|
||||
<div class="field"><label class="label" for="new2">Repeat</label><div class="control"><input class="input" id="new2" name="new_password_confirm" type="password" autocomplete="new-password" required></div></div>
|
||||
</div>
|
||||
<p class="help mb-3">Changing it signs out every other session of this account; this one stays signed in.</p>
|
||||
<button type="submit" class="button">Change password</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<footer class="sp-foot">SelfPost 1.8.0 · © 2026 Mix · <a href="#">License (AGPL-3.0)</a> · <a href="#">Source</a> · No warranty</footer>
|
||||
</div>
|
||||
</main>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,55 @@
|
||||
/* Rendered-page audit for the panel: what check.py cannot see because it needs
|
||||
a browser. Open any page of the mockups (or of the running panel), paste this
|
||||
file into the console, then:
|
||||
|
||||
await panelAudit(["overview.html", "out-domain.html", ...]) // same-origin URLs
|
||||
await panelAudit() // the page you are on
|
||||
|
||||
It loads each page in a 1440px and a 1280px frame and reports
|
||||
- text under the 13px floor (the postmark's imprint excepted),
|
||||
- text under 4.5:1 against the ground it actually sits on,
|
||||
- anything wider than its box, and tables that have to scroll,
|
||||
- health-card values that got clipped.
|
||||
An empty `problems` list is the pass. Attach the output to the step's evidence
|
||||
(docs/plans/panel-redesign.md, Enforcement). */
|
||||
async function panelAudit(urls = [location.href], widths = [1440, 1280]) {
|
||||
const lum = (c) => {
|
||||
const [r, g, b] = c.map((v) => ((v /= 255) <= 0.03928 ? v / 12.92 : ((v + 0.055) / 1.055) ** 2.4));
|
||||
return 0.2126 * r + 0.7152 * g + 0.0722 * b;
|
||||
};
|
||||
const rgba = (s) => { const m = s.match(/[\d.]+/g).map(Number); return { rgb: m.slice(0, 3), a: m.length > 3 ? m[3] : 1 }; };
|
||||
const ground = (el, win) => {
|
||||
for (let e = el; e; e = e.parentElement) { const b = rgba(win.getComputedStyle(e).backgroundColor); if (b.a > 0.5) return b.rgb; }
|
||||
return [255, 255, 255];
|
||||
};
|
||||
const problems = new Set();
|
||||
let texts = 0;
|
||||
for (const url of urls) for (const width of widths) {
|
||||
const f = document.createElement("iframe");
|
||||
f.width = width; f.height = 900; f.src = url;
|
||||
document.body.appendChild(f);
|
||||
await new Promise((r) => (f.onload = r));
|
||||
await f.contentDocument.fonts.ready;
|
||||
await new Promise((r) => setTimeout(r, 150));
|
||||
const d = f.contentDocument, win = f.contentWindow, at = `${width} ${url.split("/").pop()}`;
|
||||
const walker = d.createTreeWalker(d.body, NodeFilter.SHOW_TEXT);
|
||||
for (let n; (n = walker.nextNode()); ) {
|
||||
const text = n.textContent.trim(), el = n.parentElement;
|
||||
if (!text || el.closest(".sp-log")) continue; // the log pane has its own dark ground and palette
|
||||
texts++;
|
||||
const cs = win.getComputedStyle(el), what = `${el.tagName.toLowerCase()}.${[...el.classList].join(".")} "${text.slice(0, 24)}"`;
|
||||
if (parseFloat(cs.fontSize) < 12.9 && !el.closest(".sp-postmark")) problems.add(`${at}: ${parseFloat(cs.fontSize)}px ${what}`);
|
||||
const fg = rgba(cs.color).rgb, bg = ground(el, win), a = lum(fg), b = lum(bg);
|
||||
const ratio = (Math.max(a, b) + 0.05) / (Math.min(a, b) + 0.05);
|
||||
const large = parseFloat(cs.fontSize) >= 24 || (parseFloat(cs.fontSize) >= 18.6 && +cs.fontWeight >= 600);
|
||||
if (ratio < (large ? 3 : 4.5)) problems.add(`${at}: contrast ${ratio.toFixed(2)} ${what}`);
|
||||
}
|
||||
if (d.documentElement.scrollWidth > win.innerWidth) problems.add(`${at}: the page scrolls sideways`);
|
||||
for (const e of d.querySelectorAll(".box, .sp-head, .sp-box-head")) if (e.scrollWidth > e.clientWidth + 1) problems.add(`${at}: overflow in .${e.className}`);
|
||||
for (const e of d.querySelectorAll(".table-container")) if (e.scrollWidth > e.clientWidth + 1) problems.add(`${at}: a table scrolls by ${e.scrollWidth - e.clientWidth}px`);
|
||||
for (const e of d.querySelectorAll(".sp-h-name, .sp-h-value, .sp-h-sub")) if (e.scrollWidth > e.clientWidth + 1) problems.add(`${at}: clipped "${e.textContent}"`);
|
||||
for (const i of d.querySelectorAll("i.ti")) if (win.getComputedStyle(i, "::before").content === "none") problems.add(`${at}: missing icon ${i.className}`);
|
||||
f.remove();
|
||||
}
|
||||
return { pages: urls.length, widths, textsChecked: texts, problems: [...problems] };
|
||||
}
|
||||
@@ -0,0 +1,116 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en" data-theme="light">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>Backup · SelfPost</title>
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bulma@1.0.4/css/bulma.min.css">
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@tabler/icons-webfont@3/dist/tabler-icons.min.css">
|
||||
<link rel="stylesheet" href="../shared/brand.css">
|
||||
<link rel="stylesheet" href="theme.css">
|
||||
</head>
|
||||
<body>
|
||||
<nav class="navbar is-primary" aria-label="main navigation">
|
||||
<div class="container">
|
||||
<div class="navbar-brand"><a class="navbar-item" href="overview.html"><img src="../../selfpost-wordmark.svg" alt="SelfPost"></a></div>
|
||||
<div class="navbar-menu is-active">
|
||||
<div class="navbar-start">
|
||||
<a class="navbar-item" href="overview.html">Overview</a>
|
||||
<div class="navbar-item has-dropdown is-hoverable">
|
||||
<a class="navbar-link" href="out-domains.html">Outbound</a>
|
||||
<div class="navbar-dropdown">
|
||||
<a class="navbar-item" href="out-domains.html"><i class="ti ti-world-upload"></i>Domains</a>
|
||||
<a class="navbar-item" href="out-log.html"><i class="ti ti-list-details"></i>Log</a>
|
||||
<a class="navbar-item" href="out-queue.html"><i class="ti ti-stack-2"></i>Queue</a>
|
||||
<a class="navbar-item" href="dmarc.html"><i class="ti ti-report-analytics"></i>DMARC reports</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="navbar-item has-dropdown is-hoverable">
|
||||
<a class="navbar-link" href="in-domains.html">Inbound</a>
|
||||
<div class="navbar-dropdown">
|
||||
<a class="navbar-item" href="in-domains.html"><i class="ti ti-world-download"></i>Domains</a>
|
||||
<a class="navbar-item" href="in-log.html"><i class="ti ti-list-details"></i>Log</a>
|
||||
<a class="navbar-item" href="in-queue.html"><i class="ti ti-stack-2"></i>Queue</a>
|
||||
<a class="navbar-item" href="in-quarantine.html"><i class="ti ti-mail-pause"></i>Quarantine</a>
|
||||
<a class="navbar-item" href="in-filter-lists.html"><i class="ti ti-filter"></i>Filter lists</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="navbar-item has-dropdown is-hoverable sp-current">
|
||||
<a class="navbar-link" href="health.html">Server</a>
|
||||
<div class="navbar-dropdown">
|
||||
<a class="navbar-item" href="health.html"><i class="ti ti-heartbeat"></i>Health</a>
|
||||
<a class="navbar-item" href="preflight.html"><i class="ti ti-checklist"></i>Preflight</a>
|
||||
<a class="navbar-item" href="system-log.html"><i class="ti ti-file-text"></i>System log</a>
|
||||
<a class="navbar-item is-active" href="backup.html"><i class="ti ti-archive"></i>Backup</a>
|
||||
<a class="navbar-item" href="users.html"><i class="ti ti-users"></i>Users</a>
|
||||
<a class="navbar-item" href="settings.html"><i class="ti ti-settings"></i>Settings</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="navbar-end">
|
||||
<div class="navbar-item has-dropdown is-hoverable">
|
||||
<a class="navbar-link" href="account.html"><i class="ti ti-user-circle"></i>admin</a>
|
||||
<div class="navbar-dropdown is-right">
|
||||
<a class="navbar-item" href="account.html"><i class="ti ti-key"></i>Account</a>
|
||||
<a class="navbar-item" href="help.html"><i class="ti ti-help"></i>Help</a>
|
||||
<hr class="navbar-divider">
|
||||
<form method="post" action="login.html"><button type="submit" class="navbar-item"><i class="ti ti-logout"></i>Sign out</button></form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
<div class="sp-perf" aria-hidden="true"></div>
|
||||
<div class="sp-subnav"><div class="container"><a href="health.html"><i class="ti ti-heartbeat"></i>Health</a><a href="preflight.html"><i class="ti ti-checklist"></i>Preflight</a><a href="system-log.html"><i class="ti ti-file-text"></i>System log</a><a href="backup.html" aria-current="page"><i class="ti ti-archive"></i>Backup</a><a href="users.html"><i class="ti ti-users"></i>Users</a><a href="settings.html"><i class="ti ti-settings"></i>Settings</a></div></div>
|
||||
<main>
|
||||
<div class="container sp-page">
|
||||
<div class="sp-head">
|
||||
<div class="sp-grow">
|
||||
<p class="sp-kicker">Server</p>
|
||||
<h1 class="title is-3">Backup</h1>
|
||||
<p class="sp-lead">Take the whole instance with you, or bring one domain in from another SelfPost. Both files are secrets.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="columns">
|
||||
<div class="column is-6">
|
||||
<div class="box">
|
||||
<div class="sp-box-head"><span class="sp-no">01</span><h2>Full backup</h2><a class="sp-help" href="help.html#backup" title="Backups, encryption and restore"><i class="ti ti-help-circle"></i></a></div>
|
||||
<div class="sp-box-body">
|
||||
<p class="mb-3"><code>data/</code> (database, DKIM keys, credentials, the queue), <code>docker-compose.yml</code>, <code>.env</code> and <code>certs/</code> in one archive. Restore into an empty project directory and start the <strong>same SelfPost version</strong>. The reverse-proxy vhost is not included.</p>
|
||||
<form class="sp-form" method="post" action="#">
|
||||
<div class="field"><label class="checkbox"><input type="checkbox" name="encrypt" checked> Encrypt with a password (<code>.spbk</code>)</label></div>
|
||||
<div class="sp-pair">
|
||||
<div class="field"><label class="label" for="pw">Password</label><div class="control"><input class="input" id="pw" type="password" placeholder="at least 12 characters"></div></div>
|
||||
<div class="field"><label class="label" for="pw2">Repeat password</label><div class="control"><input class="input" id="pw2" type="password"></div></div>
|
||||
</div>
|
||||
<p class="help mb-3">Keep this password: SelfPost does not store it, and the file cannot be opened without it.</p>
|
||||
<button type="submit" class="button is-primary"><i class="ti ti-download"></i>Download full backup</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="column is-6">
|
||||
<div class="box" id="import">
|
||||
<div class="sp-box-head"><span class="sp-no">02</span><h2>Import a domain</h2><a class="sp-help" href="help.html#backup" title="Moving a domain to another instance"><i class="ti ti-help-circle"></i></a></div>
|
||||
<div class="sp-box-body">
|
||||
<p class="mb-3">A domain export from another instance — plain <code>.json</code> or encrypted <code>.spde</code>. Its DKIM key and application passwords come across, so the published DNS needs no change. The export itself is made on the <a href="out-domain-settings.html">domain's settings</a>.</p>
|
||||
<form class="sp-form" method="post" action="out-domain.html" enctype="multipart/form-data">
|
||||
<div class="field"><label class="label">Domain export file</label>
|
||||
<div class="file has-name is-fullwidth"><label class="file-label"><input class="file-input" type="file" name="file" accept=".json,.spde"><span class="file-cta"><i class="ti ti-upload"></i><span class="file-label ml-2">Choose a file…</span></span><span class="file-name">example.org.spde</span></label></div></div>
|
||||
<div class="field"><label class="label" for="ipw">Password</label><div class="control"><input class="input" id="ipw" name="import_password" type="password" autocomplete="off"></div>
|
||||
<p class="help">Asked only for an encrypted <code>.spde</code> file.</p></div>
|
||||
<button type="submit" class="button is-primary"><i class="ti ti-file-import"></i>Import domain</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<footer class="sp-foot">SelfPost 1.8.0 · © 2026 Mix · <a href="#">License (AGPL-3.0)</a> · <a href="#">Source</a> · No warranty</footer>
|
||||
</div>
|
||||
</main>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,123 @@
|
||||
"""Wrap every fragment in src/ with the panel shell and write it next to this file.
|
||||
|
||||
The mockups are static HTML, but thirty pages share one navbar; keeping that
|
||||
navbar in one place is the only reason this script exists. Run it from
|
||||
anywhere: python docs/assets/panel-redesign/panel/build.py
|
||||
|
||||
A fragment starts with one meta comment:
|
||||
<!-- title: Outbound domains | section: outbound | page: domains -->
|
||||
section/page pick the highlighted entries; "shell: bare" skips the navbar
|
||||
(sign-in, setup).
|
||||
"""
|
||||
import pathlib
|
||||
import re
|
||||
|
||||
HERE = pathlib.Path(__file__).parent
|
||||
|
||||
NAV = [
|
||||
("overview", "Overview", "overview.html", "ti-activity-heartbeat", []),
|
||||
("outbound", "Outbound", "out-domains.html", "ti-send", [
|
||||
("domains", "Domains", "out-domains.html", "ti-world-upload"),
|
||||
("log", "Log", "out-log.html", "ti-list-details"),
|
||||
("queue", "Queue", "out-queue.html", "ti-stack-2"),
|
||||
("dmarc", "DMARC reports", "dmarc.html", "ti-report-analytics"),
|
||||
]),
|
||||
("inbound", "Inbound", "in-domains.html", "ti-inbox", [
|
||||
("domains", "Domains", "in-domains.html", "ti-world-download"),
|
||||
("log", "Log", "in-log.html", "ti-list-details"),
|
||||
("queue", "Queue", "in-queue.html", "ti-stack-2"),
|
||||
("quarantine", "Quarantine", "in-quarantine.html", "ti-mail-pause"),
|
||||
("filter-lists", "Filter lists", "in-filter-lists.html", "ti-filter"),
|
||||
]),
|
||||
("server", "Server", "health.html", "ti-server-2", [
|
||||
("health", "Health", "health.html", "ti-heartbeat"),
|
||||
("preflight", "Preflight", "preflight.html", "ti-checklist"),
|
||||
("system-log", "System log", "system-log.html", "ti-file-text"),
|
||||
("backup", "Backup", "backup.html", "ti-archive"),
|
||||
("users", "Users", "users.html", "ti-users"),
|
||||
("settings", "Settings", "settings.html", "ti-settings"),
|
||||
]),
|
||||
]
|
||||
|
||||
HEAD = """<!DOCTYPE html>
|
||||
<html lang="en" data-theme="light">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>{title} · SelfPost</title>
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bulma@1.0.4/css/bulma.min.css">
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@tabler/icons-webfont@3/dist/tabler-icons.min.css">
|
||||
<link rel="stylesheet" href="../shared/brand.css">
|
||||
<link rel="stylesheet" href="theme.css">
|
||||
</head>
|
||||
"""
|
||||
|
||||
FOOT = ('<footer class="sp-foot">SelfPost 1.8.0 · © 2026 Mix · <a href="#">License (AGPL-3.0)</a>'
|
||||
' · <a href="#">Source</a> · No warranty</footer>')
|
||||
|
||||
|
||||
def navbar(section, page):
|
||||
out = ['<nav class="navbar is-primary" aria-label="main navigation">', ' <div class="container">',
|
||||
' <div class="navbar-brand"><a class="navbar-item" href="overview.html">'
|
||||
'<img src="../../selfpost-wordmark.svg" alt="SelfPost"></a></div>',
|
||||
' <div class="navbar-menu is-active">', ' <div class="navbar-start">']
|
||||
for key, label, href, _icon, pages in NAV:
|
||||
cur = " sp-current" if key == section else ""
|
||||
if not pages:
|
||||
out.append(f' <a class="navbar-item{cur}" href="{href}">{label}</a>')
|
||||
continue
|
||||
out.append(f' <div class="navbar-item has-dropdown is-hoverable{cur}">')
|
||||
out.append(f' <a class="navbar-link" href="{href}">{label}</a>')
|
||||
out.append(' <div class="navbar-dropdown">')
|
||||
for pkey, plabel, phref, picon in pages:
|
||||
act = " is-active" if key == section and pkey == page else ""
|
||||
out.append(f' <a class="navbar-item{act}" href="{phref}"><i class="ti {picon}"></i>{plabel}</a>')
|
||||
out.append(' </div>')
|
||||
out.append(' </div>')
|
||||
out += [' </div>', ' <div class="navbar-end">',
|
||||
' <div class="navbar-item has-dropdown is-hoverable%s">' % (" sp-current" if section == "user" else ""),
|
||||
' <a class="navbar-link" href="account.html"><i class="ti ti-user-circle"></i>admin</a>',
|
||||
' <div class="navbar-dropdown is-right">',
|
||||
' <a class="navbar-item" href="account.html"><i class="ti ti-key"></i>Account</a>',
|
||||
' <a class="navbar-item" href="help.html"><i class="ti ti-help"></i>Help</a>',
|
||||
' <hr class="navbar-divider">',
|
||||
# Signing out changes state, so it is a POST: a form with a button, not a link.
|
||||
' <form method="post" action="login.html"><button type="submit" class="navbar-item">'
|
||||
'<i class="ti ti-logout"></i>Sign out</button></form>',
|
||||
' </div>', ' </div>', ' </div>', ' </div>', ' </div>', '</nav>',
|
||||
'<div class="sp-perf" aria-hidden="true"></div>']
|
||||
return "\n".join(out)
|
||||
|
||||
|
||||
def subnav(section, page):
|
||||
for key, _label, _href, _icon, pages in NAV:
|
||||
if key == section and pages:
|
||||
cur = ' aria-current="page"'
|
||||
links = "".join(
|
||||
f'<a href="{phref}"{cur if pkey == page else ""}><i class="ti {picon}"></i>{plabel}</a>'
|
||||
for pkey, plabel, phref, picon in pages)
|
||||
return f'<div class="sp-subnav"><div class="container">{links}</div></div>'
|
||||
return ""
|
||||
|
||||
|
||||
def build(path):
|
||||
text = path.read_text(encoding="utf-8")
|
||||
m = re.match(r"\s*<!--(.*?)-->\s*", text, re.S)
|
||||
meta = dict(part.split(":", 1) for part in m.group(1).split("|"))
|
||||
meta = {k.strip(): v.strip() for k, v in meta.items()}
|
||||
body = text[m.end():]
|
||||
html = HEAD.format(title=meta["title"])
|
||||
if meta.get("shell") == "bare":
|
||||
html += f"<body>\n{body}\n</body>\n</html>\n"
|
||||
else:
|
||||
section, page = meta.get("section", ""), meta.get("page", "")
|
||||
html += (f"<body>\n{navbar(section, page)}\n{subnav(section, page)}\n<main>\n"
|
||||
f' <div class="container sp-page">\n{body}\n {FOOT}\n </div>\n</main>\n</body>\n</html>\n')
|
||||
(HERE / path.name).write_text(html, encoding="utf-8", newline="\n")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
pages = sorted((HERE / "src").glob("*.html"))
|
||||
for p in pages:
|
||||
build(p)
|
||||
print(f"built {len(pages)} pages")
|
||||
@@ -0,0 +1,240 @@
|
||||
"""Design-contract lint for the panel mockups, and the source of outlines.json.
|
||||
|
||||
The contract (docs/plans/panel-redesign.md) is only worth something if a
|
||||
machine can say "no". This script is that machine for the mockups; the same
|
||||
rules are ported to a Go test over internal/web templates when the redesign is
|
||||
implemented, and that test compares rendered pages with outlines.json.
|
||||
|
||||
python docs/assets/panel-redesign/panel/check.py # lint
|
||||
python docs/assets/panel-redesign/panel/check.py --write # lint + rewrite outlines.json
|
||||
|
||||
Exit code 1 on any violation.
|
||||
"""
|
||||
import json
|
||||
import pathlib
|
||||
import re
|
||||
import sys
|
||||
from html.parser import HTMLParser
|
||||
|
||||
HERE = pathlib.Path(__file__).parent
|
||||
VOID = {"input", "img", "br", "hr", "meta", "link"}
|
||||
|
||||
# The subset of Bulma the panel uses. Reaching for a Bulma class outside this
|
||||
# list is a design decision, not an implementation detail: add it here and to
|
||||
# the components page in the same change, or do without it.
|
||||
BULMA = set("""
|
||||
box button buttons checkbox column columns control field file file-cta file-input file-label
|
||||
file-name has-addons has-icons-left has-name has-text-danger help icon input label menu menu-label
|
||||
menu-list notification progress select table table-container tag textarea title
|
||||
is-3 is-4 is-5 is-6 is-7 is-9 is-active is-danger is-expanded is-fullwidth is-gapless is-half
|
||||
is-hidden is-hoverable is-left is-light is-primary is-small is-success is-text is-warning
|
||||
mb-1 mb-2 mb-3 mb-4 ml-2 mt-4 mt-5
|
||||
""".split())
|
||||
# Classes the shell (build.py, later layout.html) owns; screens never write them.
|
||||
SHELL = {"sp-page", "sp-perf", "sp-subnav", "sp-current", "sp-foot"}
|
||||
# The signed-out screens have no shell, so they carry their own footer.
|
||||
SIGNED_OUT = {"sp-signin", "sp-signin-brand", "sp-signin-form", "sp-foot"}
|
||||
# Selectors allowed to set a width. Everything else gets its width from the
|
||||
# layout it sits in — that is the whole point of the redesign.
|
||||
WIDTH_OK = (".navbar-brand img", ".sp-head .sp-lead", ".sp-head .sp-grow", ".sp-postmark", ".table td.sp-clip",
|
||||
".sp-h-icon", ".sp-signin", ".sp-timeline li::before", ".sp-perf")
|
||||
|
||||
|
||||
class Node:
|
||||
def __init__(self, tag, attrs, parent):
|
||||
self.tag, self.attrs, self.parent, self.kids, self.text = tag, dict(attrs), parent, [], ""
|
||||
|
||||
@property
|
||||
def classes(self):
|
||||
return (self.attrs.get("class") or "").split()
|
||||
|
||||
def has(self, cls):
|
||||
return cls in self.classes
|
||||
|
||||
def walk(self):
|
||||
yield self
|
||||
for k in self.kids:
|
||||
yield from k.walk()
|
||||
|
||||
def find(self, cls):
|
||||
return [n for n in self.walk() if n.has(cls)]
|
||||
|
||||
|
||||
class Tree(HTMLParser):
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self.root = Node("root", [], None)
|
||||
self.cur = self.root
|
||||
|
||||
def handle_starttag(self, tag, attrs):
|
||||
n = Node(tag, attrs, self.cur)
|
||||
self.cur.kids.append(n)
|
||||
if tag not in VOID:
|
||||
self.cur = n
|
||||
|
||||
def handle_endtag(self, tag):
|
||||
n = self.cur
|
||||
while n is not self.root and n.tag != tag:
|
||||
n = n.parent
|
||||
if n is not self.root:
|
||||
self.cur = n.parent
|
||||
|
||||
def handle_data(self, data):
|
||||
self.cur.text += data
|
||||
|
||||
|
||||
def parse(text):
|
||||
t = Tree()
|
||||
t.feed(text)
|
||||
return t.root
|
||||
|
||||
|
||||
def text_of(node):
|
||||
return re.sub(r"\s+", " ", "".join(n.text for n in node.walk())).strip()
|
||||
|
||||
|
||||
def css_rules(css):
|
||||
css = re.sub(r"/\*.*?\*/", "", css, flags=re.S)
|
||||
css = re.sub(r"@media[^{]*\{", "", css)
|
||||
for sel, body in re.findall(r"([^{}]+)\{([^{}]*)\}", css):
|
||||
yield sel.strip(), body
|
||||
|
||||
|
||||
def box_kind(box):
|
||||
kinds = []
|
||||
for k in box.kids:
|
||||
if k.has("sp-box-head") or k.has("sp-box-foot"):
|
||||
continue
|
||||
for cls, name in (("sp-record", "record"), ("sp-health", "health"), ("sp-empty", "empty"),
|
||||
("sp-log", "log"), ("sp-help-topic", "help"), ("sp-box-body", "body")):
|
||||
if k.has(cls):
|
||||
kinds.append(name)
|
||||
break
|
||||
else:
|
||||
kinds.append("table" if k.tag == "table" or k.has("table-container") else k.tag)
|
||||
out = []
|
||||
for k in kinds:
|
||||
if not out or out[-1].split("×")[0] != k:
|
||||
out.append(k)
|
||||
else:
|
||||
n = int(out[-1].split("×")[1]) + 1 if "×" in out[-1] else 2
|
||||
out[-1] = f"{k}×{n}"
|
||||
return "+".join(out)
|
||||
|
||||
|
||||
def outline(node):
|
||||
"""The component skeleton of a page: what a reviewer sees with the text blurred."""
|
||||
out = []
|
||||
for k in node.kids:
|
||||
if k.has("sp-head"):
|
||||
parts = ["head"] + [p for p, c in (("postmark", "sp-postmark"), ("actions", "buttons")) if k.find(c)]
|
||||
if not k.find("buttons") and any(n.tag in ("a", "form") and n.parent is k for n in k.walk()):
|
||||
parts.append("actions")
|
||||
out.append("+".join(parts))
|
||||
elif k.has("box"):
|
||||
variant = next((v for v in ("sp-danger-zone", "sp-credential") if k.has(v)), "")
|
||||
head = k.find("sp-box-head")
|
||||
title = text_of(head[0].find("sp-no")[0].parent.kids[1]) if head else "?"
|
||||
out.append(f"box{'.' + variant if variant else ''}[{title}]:{box_kind(k)}")
|
||||
elif k.has("columns"):
|
||||
cols = []
|
||||
for c in k.kids:
|
||||
size = next((x for x in c.classes if re.fullmatch(r"is-\d+", x)), "auto")
|
||||
cols.append({size: outline(c)})
|
||||
out.append({"columns": cols})
|
||||
elif k.tag == "form" or k.has("menu") or k.tag == "aside":
|
||||
out.append({"form": outline(k)} if k.tag == "form" else "side_menu")
|
||||
elif k.has("buttons"):
|
||||
out.append("submit_row")
|
||||
return out
|
||||
|
||||
|
||||
def main():
|
||||
errors = []
|
||||
css = (HERE / "theme.css").read_text(encoding="utf-8")
|
||||
defined = set(re.findall(r"\.(sp-[a-z0-9-]+)", css))
|
||||
|
||||
for sel, body in css_rules(css):
|
||||
if "!important" in body:
|
||||
errors.append(f"theme.css: !important in `{sel}`")
|
||||
if re.search(r"(^|[\s,>+~])#[a-z]", sel):
|
||||
errors.append(f"theme.css: id selector `{sel}` — style components, not one element on one page")
|
||||
# The type floor: 13px. Icons (.ti) and the postmark imprint are not running text.
|
||||
for value, unit in re.findall(r"font-size\s*:\s*([\d.]+)(rem|em|px)", body):
|
||||
floor = {"rem": 0.8125, "em": 0.85, "px": 13}[unit]
|
||||
if float(value) < floor and ".sp-postmark" not in sel and ".ti" not in sel:
|
||||
errors.append(f"theme.css: `{sel}` sets font-size {value}{unit}, below the 13px floor — use --sp-fs-xs or --sp-fs-s")
|
||||
# `min-width: 0` sizes nothing: it only lets a flex or grid child shrink so its text can be clipped.
|
||||
sized = re.sub(r"min-width\s*:\s*0\s*(;|$)", "", body)
|
||||
if re.search(r"(?<![-\w])(max-width|min-width|width)\s*:", sized) and not any(sel.startswith(w) or w in sel for w in WIDTH_OK):
|
||||
errors.append(f"theme.css: `{sel}` sets a width; widths come from the page layout (WIDTH_OK)")
|
||||
|
||||
used, documented, outlines = set(), set(), {}
|
||||
for src in sorted((HERE / "src").glob("*.html")):
|
||||
text = src.read_text(encoding="utf-8")
|
||||
name = src.stem
|
||||
bare = "shell: bare" in text.split("-->")[0]
|
||||
for bad in ("style=", "<style", "<script", " onclick=", " id=\"page-"):
|
||||
if bad in text:
|
||||
errors.append(f"{name}: `{bad}` is forbidden in a screen")
|
||||
root = parse(text)
|
||||
for n in root.walk():
|
||||
for c in n.classes:
|
||||
if c.startswith("sp-"):
|
||||
(documented if name == "components" else used).add(c)
|
||||
if c in SHELL and not (bare and c in SIGNED_OUT):
|
||||
errors.append(f"{name}: `{c}` belongs to the shell")
|
||||
elif c not in defined:
|
||||
errors.append(f"{name}: `{c}` is not defined in theme.css")
|
||||
elif c == "ti" or c.startswith("ti-"):
|
||||
continue
|
||||
elif c == "is-small" and "progress" not in n.classes:
|
||||
errors.append(f"{name}: `is-small` on <{n.tag}> — controls keep the normal size; only a progress bar is small")
|
||||
elif c not in BULMA:
|
||||
errors.append(f"{name}: class `{c}` is neither in the Bulma subset nor an sp- component")
|
||||
if bare:
|
||||
continue
|
||||
top = [k for k in root.kids if k.tag != "root"]
|
||||
if not top or not top[0].has("sp-head"):
|
||||
errors.append(f"{name}: the page must open with page_head (.sp-head)")
|
||||
if len([n for n in root.walk() if n.tag == "h1"]) != 1:
|
||||
errors.append(f"{name}: exactly one <h1>")
|
||||
marks = root.find("sp-postmark")
|
||||
if name != "components" and (len(marks) > 1 or any(not m.parent.has("sp-head") for m in marks)):
|
||||
errors.append(f"{name}: at most one postmark, inside page_head")
|
||||
for tbl in (n for n in root.walk() if n.tag == "table"):
|
||||
if not tbl.parent.has("table-container"):
|
||||
errors.append(f"{name}: a <table> must sit in a table-container so a wide one can scroll")
|
||||
for box in root.find("box"):
|
||||
if not box.kids or not box.kids[0].has("sp-box-head"):
|
||||
errors.append(f"{name}: a box must start with box_head")
|
||||
for k in top[1:]:
|
||||
# A flash is conditional, so it is allowed at the top level but never part of an outline.
|
||||
ok = k.has("box") or k.has("columns") or k.has("notification") or (k.tag == "form" and k.has("sp-form"))
|
||||
if not ok:
|
||||
errors.append(f"{name}: top-level <{k.tag} class=\"{' '.join(k.classes)}\"> — a page is boxes, columns or one form")
|
||||
if name != "components":
|
||||
outlines[name] = outline(root)
|
||||
|
||||
for c in sorted(used - documented - SIGNED_OUT):
|
||||
errors.append(f"components page does not show `{c}` — document it or stop using it")
|
||||
shell_used = set(re.findall(r"sp-[a-z0-9-]+", (HERE / "build.py").read_text(encoding="utf-8")))
|
||||
for c in sorted(defined - used - documented - shell_used):
|
||||
errors.append(f"theme.css defines `{c}` but nothing uses it")
|
||||
|
||||
if "--write" in sys.argv and not errors:
|
||||
(HERE / "outlines.json").write_text(json.dumps(outlines, ensure_ascii=False, indent=1) + "\n",
|
||||
encoding="utf-8", newline="\n")
|
||||
golden = HERE / "outlines.json"
|
||||
if golden.exists() and "--write" not in sys.argv:
|
||||
if json.loads(golden.read_text(encoding="utf-8")) != outlines:
|
||||
errors.append("outlines.json is stale — a screen's structure changed; re-run with --write and say so in the commit")
|
||||
|
||||
for e in errors:
|
||||
print("FAIL", e)
|
||||
print(f"{len(outlines) + 1} screens, {len(used | documented)} sp- classes, {len(errors)} violation(s)")
|
||||
return 1 if errors else 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
@@ -0,0 +1,294 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en" data-theme="light">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>Components · SelfPost</title>
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bulma@1.0.4/css/bulma.min.css">
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@tabler/icons-webfont@3/dist/tabler-icons.min.css">
|
||||
<link rel="stylesheet" href="../shared/brand.css">
|
||||
<link rel="stylesheet" href="theme.css">
|
||||
</head>
|
||||
<body>
|
||||
<nav class="navbar is-primary" aria-label="main navigation">
|
||||
<div class="container">
|
||||
<div class="navbar-brand"><a class="navbar-item" href="overview.html"><img src="../../selfpost-wordmark.svg" alt="SelfPost"></a></div>
|
||||
<div class="navbar-menu is-active">
|
||||
<div class="navbar-start">
|
||||
<a class="navbar-item" href="overview.html">Overview</a>
|
||||
<div class="navbar-item has-dropdown is-hoverable">
|
||||
<a class="navbar-link" href="out-domains.html">Outbound</a>
|
||||
<div class="navbar-dropdown">
|
||||
<a class="navbar-item" href="out-domains.html"><i class="ti ti-world-upload"></i>Domains</a>
|
||||
<a class="navbar-item" href="out-log.html"><i class="ti ti-list-details"></i>Log</a>
|
||||
<a class="navbar-item" href="out-queue.html"><i class="ti ti-stack-2"></i>Queue</a>
|
||||
<a class="navbar-item" href="dmarc.html"><i class="ti ti-report-analytics"></i>DMARC reports</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="navbar-item has-dropdown is-hoverable">
|
||||
<a class="navbar-link" href="in-domains.html">Inbound</a>
|
||||
<div class="navbar-dropdown">
|
||||
<a class="navbar-item" href="in-domains.html"><i class="ti ti-world-download"></i>Domains</a>
|
||||
<a class="navbar-item" href="in-log.html"><i class="ti ti-list-details"></i>Log</a>
|
||||
<a class="navbar-item" href="in-queue.html"><i class="ti ti-stack-2"></i>Queue</a>
|
||||
<a class="navbar-item" href="in-quarantine.html"><i class="ti ti-mail-pause"></i>Quarantine</a>
|
||||
<a class="navbar-item" href="in-filter-lists.html"><i class="ti ti-filter"></i>Filter lists</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="navbar-item has-dropdown is-hoverable">
|
||||
<a class="navbar-link" href="health.html">Server</a>
|
||||
<div class="navbar-dropdown">
|
||||
<a class="navbar-item" href="health.html"><i class="ti ti-heartbeat"></i>Health</a>
|
||||
<a class="navbar-item" href="preflight.html"><i class="ti ti-checklist"></i>Preflight</a>
|
||||
<a class="navbar-item" href="system-log.html"><i class="ti ti-file-text"></i>System log</a>
|
||||
<a class="navbar-item" href="backup.html"><i class="ti ti-archive"></i>Backup</a>
|
||||
<a class="navbar-item" href="users.html"><i class="ti ti-users"></i>Users</a>
|
||||
<a class="navbar-item" href="settings.html"><i class="ti ti-settings"></i>Settings</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="navbar-end">
|
||||
<div class="navbar-item has-dropdown is-hoverable">
|
||||
<a class="navbar-link" href="account.html"><i class="ti ti-user-circle"></i>admin</a>
|
||||
<div class="navbar-dropdown is-right">
|
||||
<a class="navbar-item" href="account.html"><i class="ti ti-key"></i>Account</a>
|
||||
<a class="navbar-item" href="help.html"><i class="ti ti-help"></i>Help</a>
|
||||
<hr class="navbar-divider">
|
||||
<form method="post" action="login.html"><button type="submit" class="navbar-item"><i class="ti ti-logout"></i>Sign out</button></form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
<div class="sp-perf" aria-hidden="true"></div>
|
||||
|
||||
<main>
|
||||
<div class="container sp-page">
|
||||
<div class="sp-head">
|
||||
<div class="sp-postmark"><span>SELFPOST</span><b>REF</b><span>DESIGN CONTRACT</span></div>
|
||||
<div class="sp-grow">
|
||||
<p class="sp-kicker">Reference · page_head</p>
|
||||
<h1 class="title is-3">Components</h1>
|
||||
<p class="sp-lead">Every building block of the panel, in every state it has. A screen is assembled from these and from plain Bulma — nothing else. The title of each box is the name of its template partial.</p>
|
||||
</div>
|
||||
<div class="buttons"><a class="button" href="../index.html"><i class="ti ti-arrow-left"></i>Cover sheet</a></div>
|
||||
</div>
|
||||
|
||||
<div class="notification is-success is-light"><i class="ti ti-circle-check"></i><strong>flash</strong> — the result of the last action, between page_head and the first box. Success, or…</div>
|
||||
<div class="notification is-danger is-light"><i class="ti ti-alert-circle"></i>…<strong>flash, error</strong> — a failure that belongs to the page, not to one field. A field's error stays under the field in Bulma's <code>help is-danger</code>.</div>
|
||||
|
||||
<div class="box">
|
||||
<div class="sp-box-head"><span class="sp-no">00</span><h2>Page layouts</h2><div class="sp-end">A page is exactly one of these</div></div>
|
||||
<div class="sp-box-body">
|
||||
<dl class="sp-facts">
|
||||
<div><dt>list</dt><dd>page_head, then full-width boxes: the add form first, the table second. <a href="out-domains.html">Example</a></dd></div>
|
||||
<div><dt>detail</dt><dd>page_head, then <code>columns</code>: <code>is-3</code> side menu + <code>is-9</code> boxes. <a href="out-domain.html">Example</a></dd></div>
|
||||
<div><dt>form</dt><dd>page_head, then boxes in equal <code>columns</code>, one submit row under them. <a href="out-app.html">Example</a></dd></div>
|
||||
<div><dt>signed out</dt><dd>The split brand / form screen, no navbar. <a href="login.html">Example</a></dd></div>
|
||||
</dl>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="columns">
|
||||
<div class="column is-6">
|
||||
<div class="box">
|
||||
<div class="sp-box-head"><span class="sp-no">01</span><h2>postmark</h2><div class="sp-end">Worst status of the page's checks</div></div>
|
||||
<div class="sp-box-body">
|
||||
<div class="buttons">
|
||||
<div class="sp-postmark"><span>SELFPOST</span><b>OK</b><span>6 OF 6 CHECKS</span></div>
|
||||
<div class="sp-postmark sp-warn"><span>SELFPOST</span><b>WARN</b><span>1 OF 6 CHECKS</span></div>
|
||||
<div class="sp-postmark sp-fail"><span>DNS</span><b>FAIL</b><span>1 OF 3 RECORDS</span></div>
|
||||
</div>
|
||||
<p class="sp-muted">Only inside page_head, at most one per page, and only where the page has a verdict to give.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="box">
|
||||
<div class="sp-box-head"><span class="sp-no">03</span><h2>status_tag</h2><div class="sp-end">One mapping, one function</div></div>
|
||||
<div class="table-container"><table class="table is-fullwidth">
|
||||
<thead><tr><th>Status</th><th>Tag</th><th>Bulma classes</th></tr></thead>
|
||||
<tbody>
|
||||
<tr><td>ok, published, running, sent</td><td><span class="tag is-success is-light">ok</span></td><td class="sp-mono sp-small">tag is-success is-light</td></tr>
|
||||
<tr><td>warn, deferred, quiet</td><td><span class="tag is-warning is-light">warn</span></td><td class="sp-mono sp-small">tag is-warning is-light</td></tr>
|
||||
<tr><td>error, fail, mismatch, bounced</td><td><span class="tag is-danger is-light">fail</span></td><td class="sp-mono sp-small">tag is-danger is-light</td></tr>
|
||||
<tr><td>unknown, off, neutral counts</td><td><span class="tag is-light">off</span></td><td class="sp-mono sp-small">tag is-light</td></tr>
|
||||
<tr><td>held: quarantine · role: global</td><td><span class="tag is-primary is-light">quarantine</span></td><td class="sp-mono sp-small">tag is-primary is-light</td></tr>
|
||||
</tbody>
|
||||
</table></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="column is-6">
|
||||
<div class="box">
|
||||
<div class="sp-box-head"><span class="sp-no">02</span><h2>box · box_head</h2><div class="sp-end">The end slot: a note, a tag, a link or a filter form</div><a class="sp-help" href="help.html#checks" title="Optional: the section's Help topic"><i class="ti ti-help-circle"></i></a></div>
|
||||
<div class="sp-box-body"><p class="mb-2">The <i class="ti ti-help-circle"></i> after the end slot is <code>sp-help</code>: an optional link to the section's topic on the Help page. It replaces today's help drawer.</p><p>box_body — padded content. A <code>table</code> or a run of <code>sp-record</code> blocks sits directly in the box, without box_body.</p></div>
|
||||
<div class="sp-box-foot"><span>box_foot — a legend, paging</span><span class="sp-end">end slot</span></div>
|
||||
</div>
|
||||
<div class="box sp-danger-zone">
|
||||
<div class="sp-box-head"><span class="sp-no"><i class="ti ti-alert-triangle"></i></span><h2>box · sp-danger-zone</h2></div>
|
||||
<div class="sp-box-body"><p>For destructive confirmations and the "delete" entry of a settings page. The number may be an icon when the box is not part of a sequence.</p></div>
|
||||
</div>
|
||||
<div class="box sp-credential">
|
||||
<div class="sp-box-head"><span class="sp-no"><i class="ti ti-eye"></i></span><h2>box · sp-credential</h2><div class="sp-end">Shown once</div></div>
|
||||
<div class="sp-box-body"><p>Only for a secret the panel will never show again.</p></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="box">
|
||||
<div class="sp-box-head"><span class="sp-no">04</span><h2>health_card</h2><div class="sp-end">Overview only · each card links to its detail</div></div>
|
||||
<div class="sp-health">
|
||||
<a href="#"><span class="sp-h-icon"><i class="ti ti-server-cog"></i></span><span><span class="sp-h-name">Name</span><span class="sp-h-value">Verdict in words</span><span class="sp-h-sub">ok · one supporting fact</span></span></a>
|
||||
<a href="#" class="sp-warn"><span class="sp-h-icon"><i class="ti ti-certificate"></i></span><span><span class="sp-h-name">Name</span><span class="sp-h-value">Expires in 12 days</span><span class="sp-h-sub">sp-warn</span></span></a>
|
||||
<a href="#" class="sp-fail"><span class="sp-h-icon"><i class="ti ti-plug-connected-x"></i></span><span><span class="sp-h-name">Name</span><span class="sp-h-value">1 of 2 answering</span><span class="sp-h-sub">sp-fail</span></span></a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="columns">
|
||||
<div class="column is-7">
|
||||
<div class="box">
|
||||
<div class="sp-box-head"><span class="sp-no">05</span><h2>dns_record · copy_field</h2><div class="sp-end">One block per record</div></div>
|
||||
<div class="sp-record">
|
||||
<div class="sp-record-head"><h3>Record</h3><span class="tag is-success is-light">published</span><small>A one-line note</small></div>
|
||||
<div class="sp-row">
|
||||
<div class="field"><label class="label">Host / name</label>
|
||||
<div class="field has-addons"><div class="control is-expanded"><input class="input" readonly value="copy_field — single line"></div><div class="control"><button type="button" class="button"><i class="ti ti-copy"></i>Copy</button></div></div></div>
|
||||
<div class="field"><label class="label">Type</label><div class="control"><input class="input" readonly value="TXT"></div></div>
|
||||
</div>
|
||||
<div class="field"><label class="label">Value to publish</label>
|
||||
<div class="field has-addons"><div class="control is-expanded"><textarea class="textarea" rows="2" readonly>copy_field — multi-line, for a DKIM key</textarea></div><div class="control"><button type="button" class="button"><i class="ti ti-copy"></i>Copy</button></div></div></div>
|
||||
</div>
|
||||
<div class="sp-record">
|
||||
<div class="sp-record-head"><h3>Record</h3><span class="tag is-danger is-light">mismatch</span><small>"In DNS now" appears only when it differs</small></div>
|
||||
<div class="field"><label class="label">In DNS now</label>
|
||||
<div class="control"><input class="input is-danger" readonly value="what the resolver returned"></div>
|
||||
<p class="help is-danger">What is wrong and the exact fix.</p></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="box">
|
||||
<div class="sp-box-head"><span class="sp-no">07</span><h2>Table conventions</h2><div class="sp-end">Always inside <code>table-container</code> · an address breaks after its @</div></div>
|
||||
<div class="table-container"><table class="table is-fullwidth is-hoverable">
|
||||
<thead><tr><th>Time, UTC</th><th>Name</th><th>sp-clip</th><th>Status</th><th></th></tr></thead>
|
||||
<tbody>
|
||||
<tr><td class="sp-mono sp-muted sp-nowrap sp-small">09-21 14:01:52</td><td><a href="#"><strong>the row's link</strong></a></td><td class="sp-clip">A long subject is clipped with an ellipsis instead of wrapping the row</td><td><span class="tag is-success is-light">sent</span></td><td class="sp-actions"><a href="#">Details</a><form method="post" action="#"><button type="submit" class="has-text-danger">Delete</button></form></td></tr>
|
||||
</tbody>
|
||||
</table></div>
|
||||
<div class="sp-box-foot"><span>Page 1 of 9</span><a href="#">Older →</a></div>
|
||||
</div>
|
||||
|
||||
<div class="box">
|
||||
<div class="sp-box-head"><span class="sp-no">09</span><h2>log_pane</h2><div class="sp-end">sp-d time · sp-w warning · sp-e error</div></div>
|
||||
<pre class="sp-log"><span class="sp-d">13:58:07</span> an ordinary line
|
||||
<span class="sp-d">13:58:09</span> <span class="sp-w">a deferred delivery — sp-w</span>
|
||||
<span class="sp-d">13:57:41</span> <span class="sp-e">a reject or a bounce — sp-e</span></pre>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="column is-5">
|
||||
<div class="box">
|
||||
<div class="sp-box-head"><span class="sp-no">06</span><h2>facts</h2></div>
|
||||
<div class="sp-box-body">
|
||||
<dl class="sp-facts">
|
||||
<div><dt>Label</dt><dd>Value</dd></div>
|
||||
<div><dt>Mono value</dt><dd class="sp-mono">4XcB7k2Jm9z1</dd></div>
|
||||
<div><dt>sp-big</dt><dd class="sp-big">5 days</dd><dd class="sp-muted">a supporting line</dd></div>
|
||||
</dl>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="box">
|
||||
<div class="sp-box-head"><span class="sp-no">08</span><h2>timeline</h2></div>
|
||||
<div class="sp-box-body">
|
||||
<ol class="sp-timeline">
|
||||
<li><time>13:58:07 UTC</time><strong>Done</strong> — default</li>
|
||||
<li class="sp-warn"><time>13:58:09 UTC</time><strong>Deferred</strong> — sp-warn</li>
|
||||
<li class="sp-fail"><time>14:40:00 UTC</time><strong>Bounced</strong> — sp-fail</li>
|
||||
<li class="sp-pending"><time>not yet</time>Expected next — sp-pending</li>
|
||||
</ol>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="box">
|
||||
<div class="sp-box-head"><span class="sp-no">10</span><h2>empty_state</h2></div>
|
||||
<div class="sp-empty"><i class="ti ti-stack-2"></i>One sentence: what is absent and why that is fine.</div>
|
||||
</div>
|
||||
|
||||
<div class="box">
|
||||
<div class="sp-box-head"><span class="sp-no">11</span><h2>confirm_list</h2></div>
|
||||
<div class="sp-box-body">
|
||||
<ul class="sp-list-check">
|
||||
<li><i class="ti ti-x"></i>each thing that will be lost, one per line;</li>
|
||||
<li><i class="ti ti-x"></i>inside an sp-danger-zone box, above the danger button.</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="columns">
|
||||
<div class="column is-3">
|
||||
<aside class="menu sp-side">
|
||||
<p class="menu-label">side_menu</p>
|
||||
<ul class="menu-list">
|
||||
<li><a class="is-active" href="#"><i class="ti ti-world-check"></i>Current <span class="tag is-danger is-light">1</span></a></li>
|
||||
<li><a href="#"><i class="ti ti-apps"></i>Section <span class="tag is-light">2</span></a></li>
|
||||
</ul>
|
||||
<p class="menu-label">Rarely changed</p>
|
||||
<ul class="menu-list">
|
||||
<li><a class="has-text-danger" href="#"><i class="ti ti-trash has-text-danger"></i>Delete</a></li>
|
||||
</ul>
|
||||
</aside>
|
||||
</div>
|
||||
<div class="column is-9">
|
||||
<div class="box">
|
||||
<div class="sp-box-head"><span class="sp-no">12</span><h2>Form rows · route · help_topic</h2></div>
|
||||
<div class="sp-box-body">
|
||||
<div class="sp-form">
|
||||
<div class="sp-row">
|
||||
<div class="field"><label class="label">sp-row — a wide field</label><div class="control"><input class="input" value="host"></div></div>
|
||||
<div class="field"><label class="label">and a short one</label><div class="control"><input class="input" value="25"></div></div>
|
||||
</div>
|
||||
<div class="sp-pair">
|
||||
<div class="field"><label class="label">sp-pair — two equal fields</label><div class="control"><input class="input" type="password"></div></div>
|
||||
<div class="field"><label class="label">Repeat</label><div class="control"><input class="input" type="password"></div><p class="help">Help text lives in Bulma's <code>help</code>.</p></div>
|
||||
</div>
|
||||
</div>
|
||||
<p class="sp-route"><span>from@example.org</span><i class="ti ti-arrow-right sp-muted"></i><span>to@example.net</span><span class="tag is-warning is-light">deferred</span></p>
|
||||
</div>
|
||||
<div class="sp-help-topic"><h3>help_topic</h3><p>A heading and a paragraph; topics stack inside one box on the Help page.</p></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="box">
|
||||
<div class="sp-box-head"><span class="sp-no">13</span><h2>shell</h2><div class="sp-end">Owned by the layout — a screen never writes it</div></div>
|
||||
<div class="sp-box-body">
|
||||
<dl class="sp-facts">
|
||||
<div><dt>Name</dt><dd>The wordmark, not the stamp: at nav size the stamp's perforation is under a pixel and its brick field dissolves into the bar. The stamp lives on sign-in, setup and the favicon.</dd></div>
|
||||
<div><dt>Edge</dt><dd>Built like the mark: the brick field, then the stamp's paper margin, and the perforation along the outer edge of that margin — round holes, flat teeth, the ink hairline. One tile, <code>perf-edge.svg</code>.</dd></div>
|
||||
<div><dt>Sibling strip</dt><dd>The pages of the current group, under the edge. Five or six entries; past that the group is split.</dd></div>
|
||||
<div><dt>Menus</dt><dd>Bulma hover dropdowns, no JavaScript. The user menu holds Account, Help and Sign out.</dd></div>
|
||||
</dl>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="box">
|
||||
<div class="sp-box-head"><span class="sp-no">14</span><h2>Type scale</h2><div class="sp-end">Three sizes · nothing to be read is under 13px</div></div>
|
||||
<div class="table-container"><table class="table is-fullwidth">
|
||||
<thead><tr><th>Size</th><th>Looks like</th><th>Used for</th></tr></thead>
|
||||
<tbody>
|
||||
<tr><td class="sp-mono sp-small">body · 16px</td><td>The quick brown fox, 203.0.113.25</td><td class="sp-muted">Text, table cells, inputs, selects, buttons, menus and their dropdowns</td></tr>
|
||||
<tr><td class="sp-mono sp-small">--sp-fs-s · 14px</td><td class="sp-small">The quick brown fox, <span class="sp-mono">203.0.113.25</span></td><td class="sp-muted">Supporting text: labels, <code>help</code>, notes in a box head or foot, mono values (<code>sp-small</code>), logs, the footer</td></tr>
|
||||
<tr><td class="sp-mono sp-small">--sp-fs-xs · 13px</td><td><span class="sp-kicker">Upper-case mono label</span> <span class="tag is-light">tag</span></td><td class="sp-muted">Upper-case mono micro-labels (kicker, table head, <code>dt</code>, box title) and tags — nothing else</td></tr>
|
||||
</tbody>
|
||||
</table></div>
|
||||
<div class="sp-box-foot">Bulma's <code>is-small</code> and <code>is-size-7</code> are not used: a control is never small, and <code>sp-small</code> replaces the helper. The postmark's fine print is an imprint, not text.</div>
|
||||
</div>
|
||||
|
||||
<footer class="sp-foot">SelfPost 1.8.0 · © 2026 Mix · <a href="#">License (AGPL-3.0)</a> · <a href="#">Source</a> · No warranty</footer>
|
||||
</div>
|
||||
</main>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,110 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en" data-theme="light">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>shop.example.org · DMARC reports · SelfPost</title>
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bulma@1.0.4/css/bulma.min.css">
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@tabler/icons-webfont@3/dist/tabler-icons.min.css">
|
||||
<link rel="stylesheet" href="../shared/brand.css">
|
||||
<link rel="stylesheet" href="theme.css">
|
||||
</head>
|
||||
<body>
|
||||
<nav class="navbar is-primary" aria-label="main navigation">
|
||||
<div class="container">
|
||||
<div class="navbar-brand"><a class="navbar-item" href="overview.html"><img src="../../selfpost-wordmark.svg" alt="SelfPost"></a></div>
|
||||
<div class="navbar-menu is-active">
|
||||
<div class="navbar-start">
|
||||
<a class="navbar-item" href="overview.html">Overview</a>
|
||||
<div class="navbar-item has-dropdown is-hoverable sp-current">
|
||||
<a class="navbar-link" href="out-domains.html">Outbound</a>
|
||||
<div class="navbar-dropdown">
|
||||
<a class="navbar-item" href="out-domains.html"><i class="ti ti-world-upload"></i>Domains</a>
|
||||
<a class="navbar-item" href="out-log.html"><i class="ti ti-list-details"></i>Log</a>
|
||||
<a class="navbar-item" href="out-queue.html"><i class="ti ti-stack-2"></i>Queue</a>
|
||||
<a class="navbar-item is-active" href="dmarc.html"><i class="ti ti-report-analytics"></i>DMARC reports</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="navbar-item has-dropdown is-hoverable">
|
||||
<a class="navbar-link" href="in-domains.html">Inbound</a>
|
||||
<div class="navbar-dropdown">
|
||||
<a class="navbar-item" href="in-domains.html"><i class="ti ti-world-download"></i>Domains</a>
|
||||
<a class="navbar-item" href="in-log.html"><i class="ti ti-list-details"></i>Log</a>
|
||||
<a class="navbar-item" href="in-queue.html"><i class="ti ti-stack-2"></i>Queue</a>
|
||||
<a class="navbar-item" href="in-quarantine.html"><i class="ti ti-mail-pause"></i>Quarantine</a>
|
||||
<a class="navbar-item" href="in-filter-lists.html"><i class="ti ti-filter"></i>Filter lists</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="navbar-item has-dropdown is-hoverable">
|
||||
<a class="navbar-link" href="health.html">Server</a>
|
||||
<div class="navbar-dropdown">
|
||||
<a class="navbar-item" href="health.html"><i class="ti ti-heartbeat"></i>Health</a>
|
||||
<a class="navbar-item" href="preflight.html"><i class="ti ti-checklist"></i>Preflight</a>
|
||||
<a class="navbar-item" href="system-log.html"><i class="ti ti-file-text"></i>System log</a>
|
||||
<a class="navbar-item" href="backup.html"><i class="ti ti-archive"></i>Backup</a>
|
||||
<a class="navbar-item" href="users.html"><i class="ti ti-users"></i>Users</a>
|
||||
<a class="navbar-item" href="settings.html"><i class="ti ti-settings"></i>Settings</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="navbar-end">
|
||||
<div class="navbar-item has-dropdown is-hoverable">
|
||||
<a class="navbar-link" href="account.html"><i class="ti ti-user-circle"></i>admin</a>
|
||||
<div class="navbar-dropdown is-right">
|
||||
<a class="navbar-item" href="account.html"><i class="ti ti-key"></i>Account</a>
|
||||
<a class="navbar-item" href="help.html"><i class="ti ti-help"></i>Help</a>
|
||||
<hr class="navbar-divider">
|
||||
<form method="post" action="login.html"><button type="submit" class="navbar-item"><i class="ti ti-logout"></i>Sign out</button></form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
<div class="sp-perf" aria-hidden="true"></div>
|
||||
<div class="sp-subnav"><div class="container"><a href="out-domains.html"><i class="ti ti-world-upload"></i>Domains</a><a href="out-log.html"><i class="ti ti-list-details"></i>Log</a><a href="out-queue.html"><i class="ti ti-stack-2"></i>Queue</a><a href="dmarc.html" aria-current="page"><i class="ti ti-report-analytics"></i>DMARC reports</a></div></div>
|
||||
<main>
|
||||
<div class="container sp-page">
|
||||
<div class="sp-head">
|
||||
<div class="sp-postmark sp-warn"><span>DMARC</span><b>99.6%</b><span>PASS · 7 DAYS</span></div>
|
||||
<div class="sp-grow">
|
||||
<p class="sp-kicker"><a href="dmarc.html">DMARC reports</a> /</p>
|
||||
<h1 class="title is-3">shop.example.org</h1>
|
||||
<p class="sp-lead">11 204 pass · 41 fail in the last 7 days. Failures come from a source that is not this relay — review before tightening <code>p=</code>.</p>
|
||||
</div>
|
||||
<a class="button" href="out-domain.html"><i class="ti ti-world-upload"></i>Domain page</a>
|
||||
</div>
|
||||
|
||||
<div class="columns">
|
||||
<div class="column is-5">
|
||||
<div class="box">
|
||||
<div class="sp-box-head"><span class="sp-no">01</span><h2>Sources · 7 days</h2><a class="sp-help" href="help.html#dmarc" title="How to read pass, fail and sources"><i class="ti ti-help-circle"></i></a></div>
|
||||
<div class="table-container"><table class="table is-fullwidth">
|
||||
<thead><tr><th>Source</th><th>Pass</th><th>Fail</th><th>Disposition</th></tr></thead>
|
||||
<tbody>
|
||||
<tr><td class="sp-mono sp-small">203.0.113.25 <span class="tag is-light">this relay</span></td><td>11 204</td><td>0</td><td><span class="tag is-success is-light">none</span></td></tr>
|
||||
<tr><td class="sp-mono sp-small">198.51.100.44</td><td>0</td><td>41</td><td><span class="tag is-warning is-light">none</span></td></tr>
|
||||
</tbody>
|
||||
</table></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="column is-7">
|
||||
<div class="box">
|
||||
<div class="sp-box-head"><span class="sp-no">02</span><h2>Reports</h2></div>
|
||||
<div class="table-container"><table class="table is-fullwidth is-hoverable">
|
||||
<thead><tr><th>Received, UTC</th><th>Reporter</th><th>Window</th><th>Pass</th><th>Fail</th><th></th></tr></thead>
|
||||
<tbody>
|
||||
<tr><td class="sp-mono sp-muted sp-nowrap sp-small">09-21 04:40</td><td class="sp-muted">Outlook.com</td><td>20 Sep</td><td>1 903</td><td><span class="tag is-warning is-light">7</span></td><td class="sp-actions"><a href="dmarc-report.html">View</a></td></tr>
|
||||
<tr><td class="sp-mono sp-muted sp-nowrap sp-small">09-21 02:15</td><td class="sp-muted">google.com</td><td>20 Sep</td><td>6 410</td><td>0</td><td class="sp-actions"><a href="dmarc-report.html">View</a></td></tr>
|
||||
<tr><td class="sp-mono sp-muted sp-nowrap sp-small">09-20 04:38</td><td class="sp-muted">Outlook.com</td><td>19 Sep</td><td>1 877</td><td><span class="tag is-warning is-light">34</span></td><td class="sp-actions"><a href="dmarc-report.html">View</a></td></tr>
|
||||
</tbody>
|
||||
</table></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<footer class="sp-foot">SelfPost 1.8.0 · © 2026 Mix · <a href="#">License (AGPL-3.0)</a> · <a href="#">Source</a> · No warranty</footer>
|
||||
</div>
|
||||
</main>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,119 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en" data-theme="light">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>Outlook.com · 20 Sep · DMARC · SelfPost</title>
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bulma@1.0.4/css/bulma.min.css">
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@tabler/icons-webfont@3/dist/tabler-icons.min.css">
|
||||
<link rel="stylesheet" href="../shared/brand.css">
|
||||
<link rel="stylesheet" href="theme.css">
|
||||
</head>
|
||||
<body>
|
||||
<nav class="navbar is-primary" aria-label="main navigation">
|
||||
<div class="container">
|
||||
<div class="navbar-brand"><a class="navbar-item" href="overview.html"><img src="../../selfpost-wordmark.svg" alt="SelfPost"></a></div>
|
||||
<div class="navbar-menu is-active">
|
||||
<div class="navbar-start">
|
||||
<a class="navbar-item" href="overview.html">Overview</a>
|
||||
<div class="navbar-item has-dropdown is-hoverable sp-current">
|
||||
<a class="navbar-link" href="out-domains.html">Outbound</a>
|
||||
<div class="navbar-dropdown">
|
||||
<a class="navbar-item" href="out-domains.html"><i class="ti ti-world-upload"></i>Domains</a>
|
||||
<a class="navbar-item" href="out-log.html"><i class="ti ti-list-details"></i>Log</a>
|
||||
<a class="navbar-item" href="out-queue.html"><i class="ti ti-stack-2"></i>Queue</a>
|
||||
<a class="navbar-item is-active" href="dmarc.html"><i class="ti ti-report-analytics"></i>DMARC reports</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="navbar-item has-dropdown is-hoverable">
|
||||
<a class="navbar-link" href="in-domains.html">Inbound</a>
|
||||
<div class="navbar-dropdown">
|
||||
<a class="navbar-item" href="in-domains.html"><i class="ti ti-world-download"></i>Domains</a>
|
||||
<a class="navbar-item" href="in-log.html"><i class="ti ti-list-details"></i>Log</a>
|
||||
<a class="navbar-item" href="in-queue.html"><i class="ti ti-stack-2"></i>Queue</a>
|
||||
<a class="navbar-item" href="in-quarantine.html"><i class="ti ti-mail-pause"></i>Quarantine</a>
|
||||
<a class="navbar-item" href="in-filter-lists.html"><i class="ti ti-filter"></i>Filter lists</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="navbar-item has-dropdown is-hoverable">
|
||||
<a class="navbar-link" href="health.html">Server</a>
|
||||
<div class="navbar-dropdown">
|
||||
<a class="navbar-item" href="health.html"><i class="ti ti-heartbeat"></i>Health</a>
|
||||
<a class="navbar-item" href="preflight.html"><i class="ti ti-checklist"></i>Preflight</a>
|
||||
<a class="navbar-item" href="system-log.html"><i class="ti ti-file-text"></i>System log</a>
|
||||
<a class="navbar-item" href="backup.html"><i class="ti ti-archive"></i>Backup</a>
|
||||
<a class="navbar-item" href="users.html"><i class="ti ti-users"></i>Users</a>
|
||||
<a class="navbar-item" href="settings.html"><i class="ti ti-settings"></i>Settings</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="navbar-end">
|
||||
<div class="navbar-item has-dropdown is-hoverable">
|
||||
<a class="navbar-link" href="account.html"><i class="ti ti-user-circle"></i>admin</a>
|
||||
<div class="navbar-dropdown is-right">
|
||||
<a class="navbar-item" href="account.html"><i class="ti ti-key"></i>Account</a>
|
||||
<a class="navbar-item" href="help.html"><i class="ti ti-help"></i>Help</a>
|
||||
<hr class="navbar-divider">
|
||||
<form method="post" action="login.html"><button type="submit" class="navbar-item"><i class="ti ti-logout"></i>Sign out</button></form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
<div class="sp-perf" aria-hidden="true"></div>
|
||||
<div class="sp-subnav"><div class="container"><a href="out-domains.html"><i class="ti ti-world-upload"></i>Domains</a><a href="out-log.html"><i class="ti ti-list-details"></i>Log</a><a href="out-queue.html"><i class="ti ti-stack-2"></i>Queue</a><a href="dmarc.html" aria-current="page"><i class="ti ti-report-analytics"></i>DMARC reports</a></div></div>
|
||||
<main>
|
||||
<div class="container sp-page">
|
||||
<div class="sp-head">
|
||||
<div class="sp-grow">
|
||||
<p class="sp-kicker"><a href="dmarc.html">DMARC reports</a> / <a href="dmarc-domain.html">shop.example.org</a> /</p>
|
||||
<h1 class="title is-4">Outlook.com · 20 Sep 2026</h1>
|
||||
<p class="sp-route"><span>shop.example.org</span><span class="tag is-success is-light">1 903 pass</span><span class="tag is-warning is-light">7 fail</span></p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="columns">
|
||||
<div class="column is-6">
|
||||
<div class="box">
|
||||
<div class="sp-box-head"><span class="sp-no">01</span><h2>Report</h2><a class="sp-help" href="help.html#dmarc" title="What an aggregate report is"><i class="ti ti-help-circle"></i></a></div>
|
||||
<div class="sp-box-body">
|
||||
<dl class="sp-facts">
|
||||
<div><dt>Reporter</dt><dd>Outlook.com</dd></div>
|
||||
<div><dt>Report id</dt><dd class="sp-mono sp-small">5f1c9a0e7b2d4e61a3</dd></div>
|
||||
<div><dt>Window</dt><dd>20 Sep 00:00 – 21 Sep 00:00 UTC</dd></div>
|
||||
<div><dt>Received</dt><dd>2026-09-21 04:40 UTC</dd></div>
|
||||
</dl>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="column is-6">
|
||||
<div class="box">
|
||||
<div class="sp-box-head"><span class="sp-no">02</span><h2>Published policy</h2></div>
|
||||
<div class="sp-box-body">
|
||||
<dl class="sp-facts">
|
||||
<div><dt>p / sp / pct</dt><dd class="sp-mono">none / none / 100</dd></div>
|
||||
<div><dt>adkim / aspf</dt><dd class="sp-mono">r / r</dd></div>
|
||||
<div><dt>rua</dt><dd class="sp-mono sp-small">dmarc@mail.example.org</dd></div>
|
||||
</dl>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="box">
|
||||
<div class="sp-box-head"><span class="sp-no">03</span><h2>Records</h2><div class="sp-end">Parsed from the aggregate XML</div></div>
|
||||
<div class="table-container"><table class="table is-fullwidth">
|
||||
<thead><tr><th>Source</th><th>Count</th><th>Disposition</th><th>SPF</th><th>DKIM</th><th>Header from</th></tr></thead>
|
||||
<tbody>
|
||||
<tr><td class="sp-mono sp-small">203.0.113.25 <span class="tag is-light">this relay</span></td><td>1 903</td><td><span class="tag is-success is-light">none</span></td><td><span class="tag is-success is-light">pass</span></td><td><span class="tag is-success is-light">pass</span></td><td>shop.example.org</td></tr>
|
||||
<tr><td class="sp-mono sp-small">198.51.100.44</td><td>7</td><td><span class="tag is-warning is-light">none</span></td><td><span class="tag is-danger is-light">fail</span></td><td><span class="tag is-danger is-light">fail</span></td><td>shop.example.org</td></tr>
|
||||
</tbody>
|
||||
</table></div>
|
||||
</div>
|
||||
|
||||
<footer class="sp-foot">SelfPost 1.8.0 · © 2026 Mix · <a href="#">License (AGPL-3.0)</a> · <a href="#">Source</a> · No warranty</footer>
|
||||
</div>
|
||||
</main>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,105 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en" data-theme="light">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>DMARC reports · SelfPost</title>
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bulma@1.0.4/css/bulma.min.css">
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@tabler/icons-webfont@3/dist/tabler-icons.min.css">
|
||||
<link rel="stylesheet" href="../shared/brand.css">
|
||||
<link rel="stylesheet" href="theme.css">
|
||||
</head>
|
||||
<body>
|
||||
<nav class="navbar is-primary" aria-label="main navigation">
|
||||
<div class="container">
|
||||
<div class="navbar-brand"><a class="navbar-item" href="overview.html"><img src="../../selfpost-wordmark.svg" alt="SelfPost"></a></div>
|
||||
<div class="navbar-menu is-active">
|
||||
<div class="navbar-start">
|
||||
<a class="navbar-item" href="overview.html">Overview</a>
|
||||
<div class="navbar-item has-dropdown is-hoverable sp-current">
|
||||
<a class="navbar-link" href="out-domains.html">Outbound</a>
|
||||
<div class="navbar-dropdown">
|
||||
<a class="navbar-item" href="out-domains.html"><i class="ti ti-world-upload"></i>Domains</a>
|
||||
<a class="navbar-item" href="out-log.html"><i class="ti ti-list-details"></i>Log</a>
|
||||
<a class="navbar-item" href="out-queue.html"><i class="ti ti-stack-2"></i>Queue</a>
|
||||
<a class="navbar-item is-active" href="dmarc.html"><i class="ti ti-report-analytics"></i>DMARC reports</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="navbar-item has-dropdown is-hoverable">
|
||||
<a class="navbar-link" href="in-domains.html">Inbound</a>
|
||||
<div class="navbar-dropdown">
|
||||
<a class="navbar-item" href="in-domains.html"><i class="ti ti-world-download"></i>Domains</a>
|
||||
<a class="navbar-item" href="in-log.html"><i class="ti ti-list-details"></i>Log</a>
|
||||
<a class="navbar-item" href="in-queue.html"><i class="ti ti-stack-2"></i>Queue</a>
|
||||
<a class="navbar-item" href="in-quarantine.html"><i class="ti ti-mail-pause"></i>Quarantine</a>
|
||||
<a class="navbar-item" href="in-filter-lists.html"><i class="ti ti-filter"></i>Filter lists</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="navbar-item has-dropdown is-hoverable">
|
||||
<a class="navbar-link" href="health.html">Server</a>
|
||||
<div class="navbar-dropdown">
|
||||
<a class="navbar-item" href="health.html"><i class="ti ti-heartbeat"></i>Health</a>
|
||||
<a class="navbar-item" href="preflight.html"><i class="ti ti-checklist"></i>Preflight</a>
|
||||
<a class="navbar-item" href="system-log.html"><i class="ti ti-file-text"></i>System log</a>
|
||||
<a class="navbar-item" href="backup.html"><i class="ti ti-archive"></i>Backup</a>
|
||||
<a class="navbar-item" href="users.html"><i class="ti ti-users"></i>Users</a>
|
||||
<a class="navbar-item" href="settings.html"><i class="ti ti-settings"></i>Settings</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="navbar-end">
|
||||
<div class="navbar-item has-dropdown is-hoverable">
|
||||
<a class="navbar-link" href="account.html"><i class="ti ti-user-circle"></i>admin</a>
|
||||
<div class="navbar-dropdown is-right">
|
||||
<a class="navbar-item" href="account.html"><i class="ti ti-key"></i>Account</a>
|
||||
<a class="navbar-item" href="help.html"><i class="ti ti-help"></i>Help</a>
|
||||
<hr class="navbar-divider">
|
||||
<form method="post" action="login.html"><button type="submit" class="navbar-item"><i class="ti ti-logout"></i>Sign out</button></form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
<div class="sp-perf" aria-hidden="true"></div>
|
||||
<div class="sp-subnav"><div class="container"><a href="out-domains.html"><i class="ti ti-world-upload"></i>Domains</a><a href="out-log.html"><i class="ti ti-list-details"></i>Log</a><a href="out-queue.html"><i class="ti ti-stack-2"></i>Queue</a><a href="dmarc.html" aria-current="page"><i class="ti ti-report-analytics"></i>DMARC reports</a></div></div>
|
||||
<main>
|
||||
<div class="container sp-page">
|
||||
<div class="sp-head">
|
||||
<div class="sp-grow">
|
||||
<p class="sp-kicker">Outbound</p>
|
||||
<h1 class="title is-3">DMARC reports</h1>
|
||||
<p class="sp-lead">How receivers saw mail claiming to be from your domains. Aggregate (<code>rua=</code>) only; open a report for the parsed XML.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="box">
|
||||
<div class="sp-box-head"><span class="sp-no">01</span><h2>Ingest</h2><div class="sp-end"><span class="tag is-success is-light">ok</span></div><a class="sp-help" href="help.html#dmarc" title="What the reports are and where they go"><i class="ti ti-help-circle"></i></a></div>
|
||||
<div class="sp-box-body">
|
||||
<dl class="sp-facts">
|
||||
<div><dt>Last report</dt><dd>2026-09-21 06:12 UTC</dd></div>
|
||||
<div><dt>This week</dt><dd>14 kept · 0 parse failures</dd></div>
|
||||
<div><dt>Hosted addresses</dt><dd class="sp-mono sp-small">dmarc-reports+<domain>@mail.example.org</dd></div>
|
||||
<div><dt>Retention</dt><dd>500 reports or 90 days</dd></div>
|
||||
</dl>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="box">
|
||||
<div class="sp-box-head"><span class="sp-no">02</span><h2>Recent reports</h2></div>
|
||||
<div class="table-container"><table class="table is-fullwidth is-hoverable">
|
||||
<thead><tr><th>Received, UTC</th><th>Domain</th><th>Reporter</th><th>Window</th><th>Pass</th><th>Fail</th><th></th></tr></thead>
|
||||
<tbody>
|
||||
<tr><td class="sp-mono sp-muted sp-nowrap sp-small">09-21 06:12</td><td><a href="dmarc-domain.html">example.org</a></td><td class="sp-muted">google.com</td><td>20 Sep</td><td>412</td><td>0</td><td class="sp-actions"><a href="dmarc-report.html">View</a></td></tr>
|
||||
<tr><td class="sp-mono sp-muted sp-nowrap sp-small">09-21 04:40</td><td><a href="dmarc-domain.html">shop.example.org</a></td><td class="sp-muted">Outlook.com</td><td>20 Sep</td><td>1 903</td><td><span class="tag is-warning is-light">7</span></td><td class="sp-actions"><a href="dmarc-report.html">View</a></td></tr>
|
||||
<tr><td class="sp-mono sp-muted sp-nowrap sp-small">09-20 23:58</td><td><a href="dmarc-domain.html">example.org</a></td><td class="sp-muted">Yahoo</td><td>19 Sep</td><td>38</td><td>0</td><td class="sp-actions"><a href="dmarc-report.html">View</a></td></tr>
|
||||
<tr><td class="sp-mono sp-muted sp-nowrap sp-small">09-20 06:09</td><td><a href="dmarc-domain.html">notify.acme.io</a></td><td class="sp-muted">google.com</td><td>19 Sep</td><td>61</td><td>0</td><td class="sp-actions"><a href="dmarc-report.html">View</a></td></tr>
|
||||
</tbody>
|
||||
</table></div>
|
||||
</div>
|
||||
|
||||
<footer class="sp-foot">SelfPost 1.8.0 · © 2026 Mix · <a href="#">License (AGPL-3.0)</a> · <a href="#">Source</a> · No warranty</footer>
|
||||
</div>
|
||||
</main>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,160 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en" data-theme="light">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>Health · SelfPost</title>
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bulma@1.0.4/css/bulma.min.css">
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@tabler/icons-webfont@3/dist/tabler-icons.min.css">
|
||||
<link rel="stylesheet" href="../shared/brand.css">
|
||||
<link rel="stylesheet" href="theme.css">
|
||||
</head>
|
||||
<body>
|
||||
<nav class="navbar is-primary" aria-label="main navigation">
|
||||
<div class="container">
|
||||
<div class="navbar-brand"><a class="navbar-item" href="overview.html"><img src="../../selfpost-wordmark.svg" alt="SelfPost"></a></div>
|
||||
<div class="navbar-menu is-active">
|
||||
<div class="navbar-start">
|
||||
<a class="navbar-item" href="overview.html">Overview</a>
|
||||
<div class="navbar-item has-dropdown is-hoverable">
|
||||
<a class="navbar-link" href="out-domains.html">Outbound</a>
|
||||
<div class="navbar-dropdown">
|
||||
<a class="navbar-item" href="out-domains.html"><i class="ti ti-world-upload"></i>Domains</a>
|
||||
<a class="navbar-item" href="out-log.html"><i class="ti ti-list-details"></i>Log</a>
|
||||
<a class="navbar-item" href="out-queue.html"><i class="ti ti-stack-2"></i>Queue</a>
|
||||
<a class="navbar-item" href="dmarc.html"><i class="ti ti-report-analytics"></i>DMARC reports</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="navbar-item has-dropdown is-hoverable">
|
||||
<a class="navbar-link" href="in-domains.html">Inbound</a>
|
||||
<div class="navbar-dropdown">
|
||||
<a class="navbar-item" href="in-domains.html"><i class="ti ti-world-download"></i>Domains</a>
|
||||
<a class="navbar-item" href="in-log.html"><i class="ti ti-list-details"></i>Log</a>
|
||||
<a class="navbar-item" href="in-queue.html"><i class="ti ti-stack-2"></i>Queue</a>
|
||||
<a class="navbar-item" href="in-quarantine.html"><i class="ti ti-mail-pause"></i>Quarantine</a>
|
||||
<a class="navbar-item" href="in-filter-lists.html"><i class="ti ti-filter"></i>Filter lists</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="navbar-item has-dropdown is-hoverable sp-current">
|
||||
<a class="navbar-link" href="health.html">Server</a>
|
||||
<div class="navbar-dropdown">
|
||||
<a class="navbar-item is-active" href="health.html"><i class="ti ti-heartbeat"></i>Health</a>
|
||||
<a class="navbar-item" href="preflight.html"><i class="ti ti-checklist"></i>Preflight</a>
|
||||
<a class="navbar-item" href="system-log.html"><i class="ti ti-file-text"></i>System log</a>
|
||||
<a class="navbar-item" href="backup.html"><i class="ti ti-archive"></i>Backup</a>
|
||||
<a class="navbar-item" href="users.html"><i class="ti ti-users"></i>Users</a>
|
||||
<a class="navbar-item" href="settings.html"><i class="ti ti-settings"></i>Settings</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="navbar-end">
|
||||
<div class="navbar-item has-dropdown is-hoverable">
|
||||
<a class="navbar-link" href="account.html"><i class="ti ti-user-circle"></i>admin</a>
|
||||
<div class="navbar-dropdown is-right">
|
||||
<a class="navbar-item" href="account.html"><i class="ti ti-key"></i>Account</a>
|
||||
<a class="navbar-item" href="help.html"><i class="ti ti-help"></i>Help</a>
|
||||
<hr class="navbar-divider">
|
||||
<form method="post" action="login.html"><button type="submit" class="navbar-item"><i class="ti ti-logout"></i>Sign out</button></form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
<div class="sp-perf" aria-hidden="true"></div>
|
||||
<div class="sp-subnav"><div class="container"><a href="health.html" aria-current="page"><i class="ti ti-heartbeat"></i>Health</a><a href="preflight.html"><i class="ti ti-checklist"></i>Preflight</a><a href="system-log.html"><i class="ti ti-file-text"></i>System log</a><a href="backup.html"><i class="ti ti-archive"></i>Backup</a><a href="users.html"><i class="ti ti-users"></i>Users</a><a href="settings.html"><i class="ti ti-settings"></i>Settings</a></div></div>
|
||||
<main>
|
||||
<div class="container sp-page">
|
||||
<div class="sp-head">
|
||||
<div class="sp-grow">
|
||||
<p class="sp-kicker">Server</p>
|
||||
<h1 class="title is-3">Health</h1>
|
||||
<p class="sp-lead">Everything behind the six cards on Overview. Refreshes every 10 s.</p>
|
||||
</div>
|
||||
<form method="post" action="#"><button type="submit" class="button"><i class="ti ti-refresh"></i>Re-check DNS</button></form>
|
||||
</div>
|
||||
|
||||
<div class="columns">
|
||||
<div class="column is-6">
|
||||
<div class="box" id="machine">
|
||||
<div class="sp-box-head"><span class="sp-no">01</span><h2>Machine</h2><div class="sp-end"><span class="tag is-success is-light">ok</span></div><a class="sp-help" href="help.html#checks" title="What these readings mean"><i class="ti ti-help-circle"></i></a></div>
|
||||
<div class="table-container"><table class="table is-fullwidth">
|
||||
<thead><tr><th>Resource</th><th>Usage</th><th>Detail</th></tr></thead>
|
||||
<tbody>
|
||||
<tr><td>CPU</td><td><progress class="progress is-small is-success mb-1" value="12" max="100"></progress>12 %</td><td class="sp-muted">2 cores, load 0.21</td></tr>
|
||||
<tr><td>Memory</td><td><progress class="progress is-small is-success mb-1" value="41" max="100"></progress>41 %</td><td class="sp-muted">812 MB of 1.9 GB</td></tr>
|
||||
<tr><td>Network</td><td class="sp-nowrap">↓ 18 kB/s · ↑ 44 kB/s</td><td class="sp-muted">eth0: 2.1 GB in, 6.4 GB out</td></tr>
|
||||
</tbody>
|
||||
</table></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="column is-6">
|
||||
<div class="box" id="processes">
|
||||
<div class="sp-box-head"><span class="sp-no">02</span><h2>Processes</h2><div class="sp-end"><span class="tag is-success is-light">ok</span></div></div>
|
||||
<div class="table-container"><table class="table is-fullwidth">
|
||||
<thead><tr><th>Program</th><th>State</th><th>Detail</th></tr></thead>
|
||||
<tbody>
|
||||
<tr><td>postfix</td><td><span class="tag is-success is-light">running</span></td><td class="sp-muted">pid 41, uptime 18 days</td></tr>
|
||||
<tr><td>opendkim</td><td><span class="tag is-success is-light">running</span></td><td class="sp-muted">pid 39, uptime 18 days</td></tr>
|
||||
<tr><td>journal-milter</td><td><span class="tag is-success is-light">running</span></td><td class="sp-muted">pid 40, uptime 18 days</td></tr>
|
||||
<tr><td>panel</td><td><span class="tag is-success is-light">running</span></td><td class="sp-muted">pid 44, uptime 18 days</td></tr>
|
||||
<tr><td>dmarc-ingest</td><td><span class="tag is-success is-light">running</span></td><td class="sp-muted">pid 47, uptime 18 days</td></tr>
|
||||
</tbody>
|
||||
</table></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="columns">
|
||||
<div class="column is-4">
|
||||
<div class="box" id="certificate">
|
||||
<div class="sp-box-head"><span class="sp-no">03</span><h2>TLS certificate</h2><div class="sp-end"><span class="tag is-warning is-light">warn</span></div><a class="sp-help" href="help.html#certificate" title="When it warns and what to do"><i class="ti ti-help-circle"></i></a></div>
|
||||
<div class="sp-box-body">
|
||||
<dl class="sp-facts">
|
||||
<div><dt>Expires</dt><dd class="sp-mono">2026-10-03 09:14 UTC</dd></div>
|
||||
<div><dt>Names</dt><dd class="sp-mono">mail.example.org</dd></div>
|
||||
</dl>
|
||||
<p class="mt-4 has-text-danger">Expires in 12 days — renewal has not run.</p>
|
||||
<p class="sp-muted">Renewal normally runs 30 days before expiry. Check that port 80 is reachable and look for <code>acme</code> lines in the <a href="system-log.html">system log</a>.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="column is-4">
|
||||
<div class="box" id="sockets">
|
||||
<div class="sp-box-head"><span class="sp-no">04</span><h2>Milter sockets</h2><div class="sp-end"><span class="tag is-success is-light">ok</span></div></div>
|
||||
<div class="table-container"><table class="table is-fullwidth">
|
||||
<thead><tr><th>Milter</th><th>State</th></tr></thead>
|
||||
<tbody>
|
||||
<tr><td>opendkim<br><span class="sp-muted sp-mono sp-small">inet:127.0.0.1:8891</span></td><td><span class="tag is-success is-light">ok</span></td></tr>
|
||||
<tr><td>journal-milter<br><span class="sp-muted sp-mono sp-small">unix:/run/selfpost/journal.sock</span></td><td><span class="tag is-success is-light">ok</span></td></tr>
|
||||
<tr><td>rspamd <span class="sp-muted">· inbound spam filter</span><br><span class="sp-muted sp-mono sp-small">inet:antispam:11332</span></td><td><span class="tag is-success is-light">ok</span></td></tr>
|
||||
</tbody>
|
||||
</table></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="column is-4">
|
||||
<div class="box" id="hostname">
|
||||
<div class="sp-box-head"><span class="sp-no">05</span><h2>Hostname and reverse DNS</h2><div class="sp-end"><span class="tag is-success is-light">ok</span></div><a class="sp-help" href="help.html#rdns" title="Forward and reverse DNS"><i class="ti ti-help-circle"></i></a></div>
|
||||
<div class="sp-box-body">
|
||||
<dl class="sp-facts">
|
||||
<div><dt>Hostname</dt><dd class="sp-mono">mail.example.org</dd></div>
|
||||
<div><dt>Lookup</dt><dd class="sp-mono">→ 203.0.113.25<br>← mail.example.org</dd></div>
|
||||
</dl>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="box" id="configuration">
|
||||
<div class="sp-box-head"><span class="sp-no">06</span><h2>Configuration</h2><div class="sp-end">Safe to run at any time</div></div>
|
||||
<div class="sp-box-body">
|
||||
<p class="mb-3">Regenerates the OpenDKIM and Postfix configuration from the database and reloads both daemons. Use it after editing the files by hand, after restoring a backup, or when the running configuration looks out of step with the domain and application lists. It does not touch the queue or the TLS certificate.</p>
|
||||
<form method="post" action="#"><button type="submit" class="button"><i class="ti ti-reload"></i>Reload configuration</button></form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<footer class="sp-foot">SelfPost 1.8.0 · © 2026 Mix · <a href="#">License (AGPL-3.0)</a> · <a href="#">Source</a> · No warranty</footer>
|
||||
</div>
|
||||
</main>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,153 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en" data-theme="light">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>Help · SelfPost</title>
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bulma@1.0.4/css/bulma.min.css">
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@tabler/icons-webfont@3/dist/tabler-icons.min.css">
|
||||
<link rel="stylesheet" href="../shared/brand.css">
|
||||
<link rel="stylesheet" href="theme.css">
|
||||
</head>
|
||||
<body>
|
||||
<nav class="navbar is-primary" aria-label="main navigation">
|
||||
<div class="container">
|
||||
<div class="navbar-brand"><a class="navbar-item" href="overview.html"><img src="../../selfpost-wordmark.svg" alt="SelfPost"></a></div>
|
||||
<div class="navbar-menu is-active">
|
||||
<div class="navbar-start">
|
||||
<a class="navbar-item" href="overview.html">Overview</a>
|
||||
<div class="navbar-item has-dropdown is-hoverable">
|
||||
<a class="navbar-link" href="out-domains.html">Outbound</a>
|
||||
<div class="navbar-dropdown">
|
||||
<a class="navbar-item" href="out-domains.html"><i class="ti ti-world-upload"></i>Domains</a>
|
||||
<a class="navbar-item" href="out-log.html"><i class="ti ti-list-details"></i>Log</a>
|
||||
<a class="navbar-item" href="out-queue.html"><i class="ti ti-stack-2"></i>Queue</a>
|
||||
<a class="navbar-item" href="dmarc.html"><i class="ti ti-report-analytics"></i>DMARC reports</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="navbar-item has-dropdown is-hoverable">
|
||||
<a class="navbar-link" href="in-domains.html">Inbound</a>
|
||||
<div class="navbar-dropdown">
|
||||
<a class="navbar-item" href="in-domains.html"><i class="ti ti-world-download"></i>Domains</a>
|
||||
<a class="navbar-item" href="in-log.html"><i class="ti ti-list-details"></i>Log</a>
|
||||
<a class="navbar-item" href="in-queue.html"><i class="ti ti-stack-2"></i>Queue</a>
|
||||
<a class="navbar-item" href="in-quarantine.html"><i class="ti ti-mail-pause"></i>Quarantine</a>
|
||||
<a class="navbar-item" href="in-filter-lists.html"><i class="ti ti-filter"></i>Filter lists</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="navbar-item has-dropdown is-hoverable">
|
||||
<a class="navbar-link" href="health.html">Server</a>
|
||||
<div class="navbar-dropdown">
|
||||
<a class="navbar-item" href="health.html"><i class="ti ti-heartbeat"></i>Health</a>
|
||||
<a class="navbar-item" href="preflight.html"><i class="ti ti-checklist"></i>Preflight</a>
|
||||
<a class="navbar-item" href="system-log.html"><i class="ti ti-file-text"></i>System log</a>
|
||||
<a class="navbar-item" href="backup.html"><i class="ti ti-archive"></i>Backup</a>
|
||||
<a class="navbar-item" href="users.html"><i class="ti ti-users"></i>Users</a>
|
||||
<a class="navbar-item" href="settings.html"><i class="ti ti-settings"></i>Settings</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="navbar-end">
|
||||
<div class="navbar-item has-dropdown is-hoverable sp-current">
|
||||
<a class="navbar-link" href="account.html"><i class="ti ti-user-circle"></i>admin</a>
|
||||
<div class="navbar-dropdown is-right">
|
||||
<a class="navbar-item" href="account.html"><i class="ti ti-key"></i>Account</a>
|
||||
<a class="navbar-item" href="help.html"><i class="ti ti-help"></i>Help</a>
|
||||
<hr class="navbar-divider">
|
||||
<form method="post" action="login.html"><button type="submit" class="navbar-item"><i class="ti ti-logout"></i>Sign out</button></form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
<div class="sp-perf" aria-hidden="true"></div>
|
||||
|
||||
<main>
|
||||
<div class="container sp-page">
|
||||
<div class="sp-head">
|
||||
<div class="sp-grow">
|
||||
<p class="sp-kicker">Reference</p>
|
||||
<h1 class="title is-3">Help</h1>
|
||||
<p class="sp-lead">What each screen checks or does, and what to do when it is not green. The ? in a box's head opens its topic here.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="columns">
|
||||
<div class="column is-3">
|
||||
<aside class="menu sp-side">
|
||||
<p class="menu-label">Overview</p>
|
||||
<ul class="menu-list">
|
||||
<li><a class="is-active" href="#checks"><i class="ti ti-activity-heartbeat"></i>Health checks</a></li>
|
||||
<li><a href="#certificate"><i class="ti ti-certificate"></i>TLS certificate</a></li>
|
||||
<li><a href="#rdns"><i class="ti ti-arrows-exchange"></i>Reverse DNS</a></li>
|
||||
</ul>
|
||||
<p class="menu-label">Outbound</p>
|
||||
<ul class="menu-list">
|
||||
<li><a href="#dns"><i class="ti ti-world-check"></i>DNS records</a></li>
|
||||
<li><a href="#apps"><i class="ti ti-apps"></i>Applications</a></li>
|
||||
<li><a href="#limits"><i class="ti ti-gauge"></i>Rate limits</a></li>
|
||||
<li><a href="#log"><i class="ti ti-list-details"></i>Outbound log</a></li>
|
||||
<li><a href="#queue"><i class="ti ti-stack-2"></i>Queue and retries</a></li>
|
||||
<li><a href="#dmarc"><i class="ti ti-report-analytics"></i>DMARC reports</a></li>
|
||||
</ul>
|
||||
<p class="menu-label">Inbound</p>
|
||||
<ul class="menu-list">
|
||||
<li><a href="#inbound"><i class="ti ti-inbox"></i>Backup-MX and forwarding</a></li>
|
||||
<li><a href="#upstream"><i class="ti ti-arrow-forward-up"></i>Upstream</a></li>
|
||||
<li><a href="#recipients"><i class="ti ti-address-book"></i>Valid recipients</a></li>
|
||||
<li><a href="#filter"><i class="ti ti-shield-check"></i>Spam filter</a></li>
|
||||
</ul>
|
||||
<p class="menu-label">Server</p>
|
||||
<ul class="menu-list">
|
||||
<li><a href="#backup"><i class="ti ti-archive"></i>Backup and moving a domain</a></li>
|
||||
<li><a href="#users"><i class="ti ti-users"></i>Users and roles</a></li>
|
||||
<li><a href="#settings"><i class="ti ti-settings"></i>Settings</a></li>
|
||||
</ul>
|
||||
<p class="menu-label">Account</p>
|
||||
<ul class="menu-list">
|
||||
<li><a href="#account"><i class="ti ti-user-circle"></i>Your account</a></li>
|
||||
</ul>
|
||||
</aside>
|
||||
</div>
|
||||
<div class="column is-9">
|
||||
<div class="box">
|
||||
<div class="sp-box-head"><span class="sp-no">01</span><h2>Overview</h2></div>
|
||||
<div class="sp-help-topic" id="checks"><h3>Health checks</h3><p>Six checks, and the worst of them is the postmark. <span class="tag is-success is-light">ok</span> needs nothing; <span class="tag is-warning is-light">warn</span> still delivers mail but will stop doing so if ignored; <span class="tag is-danger is-light">fail</span> means mail is being lost or refused now. <a href="health.html">Health</a> shows the readings behind them: CPU and memory are the container's own, not the host's spare capacity, and network is a short window, not a daily total.</p></div>
|
||||
<div class="sp-help-topic" id="certificate"><h3>TLS certificate</h3><p>The certificate Postfix presents on 465 and 587. The reverse proxy or the image mounts it, the panel does not issue it. It warns 14 days before expiry and fails once expired, or when the file is missing or unreadable — clients that verify certificates then refuse to connect.</p></div>
|
||||
<div class="sp-help-topic" id="rdns"><h3>Hostname and reverse DNS</h3><p>The hostname (<code>SELFPOST_HOSTNAME</code>) must resolve to this server's address, and that address must resolve back to the hostname. Large receivers reject or spam-folder mail from hosts where the two disagree. The PTR record is set at your hosting provider, not in your DNS zone.</p></div>
|
||||
</div>
|
||||
<div class="box">
|
||||
<div class="sp-box-head"><span class="sp-no">02</span><h2>Outbound</h2></div>
|
||||
<div class="sp-help-topic" id="dns"><h3>DNS records</h3><p>DKIM proves the message was signed by this relay, SPF lists the addresses allowed to send for the domain, DMARC tells receivers what to do when both fail and where to report. Publish all three exactly as shown; merge SPF into an existing record rather than adding a second one. A domain's status is the worst of its three records, and results are cached for a few minutes, so use Re-check DNS after publishing. The SPF check is shallow: it looks for this server's address in the record itself and does not follow <code>include:</code> or <code>redirect=</code>, so a record that relies on them is reported as a warning, not a failure. <code>p=none</code> does not affect delivery; tighten to <code>quarantine</code>, then <code>reject</code>, once the reports look clean. A domain's report address is set in its Domain settings.</p></div>
|
||||
<div class="sp-help-topic" id="apps"><h3>Applications</h3><p>An application is one SASL login, unique across all domains. It may send as any address of its domain or only as listed addresses, and, with a list of client IPs set, only from those addresses. The password is shown once at creation or regeneration and is not stored in readable form.</p></div>
|
||||
<div class="sp-help-topic" id="limits"><h3>Rate limits</h3><p>Level 1 is a per-IP ceiling set in <code>.env</code>. Level 2 is optional, per domain and per application, entered by hand or computed from the last 30 days of sending, and can never exceed level 1. An application's own limit replaces its domain's.</p></div>
|
||||
<div class="sp-help-topic" id="log"><h3>Outbound log</h3><p>One row per message and recipient, with the status Postfix last reported: queued (accepted, nothing reported yet), sent (the receiving server took it; what the mailbox does next is not reported back), deferred (not taken yet, Postfix tries again), bounced (failed for good, or the queue lifetime ran out) and rejected (refused before queueing under a level-2 rate limit, so it never reached Postfix). A message's page lists these steps and the lines Postfix wrote about it in <code>mail.log</code>, found by queue id; that file is rotated daily and about two weeks are kept, so an old message may show none. Rows are deleted after the retention set in Server › Settings, 90 days by default. A domain administrator sees only the rows of their own domains.</p></div>
|
||||
<div class="sp-help-topic" id="queue"><h3>Queue and retries</h3><p>The page lists, as <code>postqueue -p</code> prints it, the messages Postfix has accepted and not yet delivered: mail being sent right now and mail deferred for a later attempt. Only the global role can open it, and it only shows; nothing here deletes or forces a message. A deferred message is tried again after the first retry delay, then at gaps that double up to a cap, until the receiving server takes it or the queue lifetime runs out and it bounces; there is no attempt counter, and the box on the page shows this server's values. A queue that keeps growing means a receiver is refusing or not answering: open the message from the Outbound log to read the reason Postfix logged.</p></div>
|
||||
<div class="sp-help-topic" id="dmarc"><h3>DMARC reports</h3><p>An aggregate report is the summary a receiving provider sends, usually daily, to the <code>rua=</code> address of your DMARC record: which servers sent mail as your domain and whether SPF or DKIM passed for it. A domain's report address is one of three choices, set in its Domain settings: no reports (the default for a new domain), the SelfPost hosted address (an address on this server whose reports are parsed and shown under DMARC reports; offered only while hosted reports are enabled on the server), or an address typed for the domain (an ordinary mailbox that receives the raw XML; a button fills in your own profile e-mail). When the typed address is on another domain than the one sending, that domain must publish a report-authorization record, or providers will not send; the domain's page shows it with the other DNS records and checks it. In the tables pass means SPF or DKIM passed and aligned, fail means neither did, and your own server shows as <em>this relay</em>. A fail from any other source is someone else sending as your domain — a forwarder, an application you forgot, or abuse — so do not tighten <code>p=</code> until that sender is fixed or removed. A domain administrator reads the reports of their own domains.</p></div>
|
||||
</div>
|
||||
<div class="box">
|
||||
<div class="sp-box-head"><span class="sp-no">03</span><h2>Inbound</h2></div>
|
||||
<div class="sp-help-topic" id="inbound"><h3>Backup-MX and forwarding</h3><p>An optional backup-MX or forwarder, switched on in the server's <code>.env</code>; the Inbound menu exists only then. Postfix accepts mail on port 25 for the listed domains only and hands each message on to that domain's upstream; there are no mailboxes. For each domain publish the MX record its page shows, and keep a primary MX the domain already has if this is a backup. The MX tag is green when at least one MX of the domain points at this server, and a domain accepts no mail until an upstream is saved on its page. Adding and deleting domains is for the global role.</p></div>
|
||||
<div class="sp-help-topic" id="upstream"><h3>Upstream</h3><p>The upstream is the server that actually holds the mailboxes. Give its host and port — used exactly as written, with no MX lookup — and choose the TLS for the hop to it: Opportunistic uses TLS when the upstream offers it and otherwise sends in the clear, Required holds the mail until a TLS session is possible, Off never uses TLS. While the upstream does not answer, Postfix keeps the mail and retries as for any other mail.</p></div>
|
||||
<div class="sp-help-topic" id="recipients"><h3>Valid recipients</h3><p>Choose whom this domain accepts. Listed addresses only: the relay accepts exactly the addresses you list, one per line or comma-separated, and refuses any other while the sender is still connected, so it never has to send a bounce for them. Any recipient: everything at the domain is accepted and passed on, and an address the upstream then rejects comes back as a bounce from this server to a sender who is often forged — backscatter. Prefer a list, and keep it in step with the mailboxes upstream.</p></div>
|
||||
<div class="sp-help-topic" id="filter"><h3>Spam filter</h3><p>The spam filter belongs to the server, not to a domain. The server's administrator attaches it in the server's <code>.env</code>, and from then on every inbound domain is screened by it before mail is handed on; the box shows on or off and has nothing to switch. Off means mail goes to the upstream unfiltered. While the filter is down, mail is either accepted unfiltered (the default) or deferred, so the sending server tries again later, as the server's setting says; the box states which.</p></div>
|
||||
</div>
|
||||
<div class="box">
|
||||
<div class="sp-box-head"><span class="sp-no">04</span><h2>Server</h2></div>
|
||||
<div class="sp-help-topic" id="backup"><h3>Backup and moving a domain</h3><p>Full backup downloads one archive of the whole instance: <code>data/</code> (the database, DKIM keys, application credentials and the queue), <code>docker-compose.yml</code>, <code>.env</code> and <code>certs/</code>. It holds secrets, so leave Encrypt with a password ticked: the file is sealed as <code>.spbk</code>, the password is not stored, and without it nothing can be recovered. To restore, unpack it into an empty project directory (decrypt a <code>.spbk</code> first with <code>selfpost-backup -decrypt</code>) and start the same SelfPost version that made it; the panel refuses data from another version. The reverse-proxy configuration and <code>mail.log</code> are not included. One domain moves on its own: Export domain on its settings page writes a <code>.json</code> (or encrypted <code>.spde</code>) with its DKIM key and its applications' passwords, and Import a domain here reads it into another instance, so the published DNS needs no change.</p></div>
|
||||
<div class="sp-help-topic" id="users"><h3>Users and roles</h3><p>The two roles are a reach, not a rank: every user is an administrator and signs in with a username and password. Global reaches the whole server — Overview, Server, the queue and every domain — and is the only role that adds or deletes domains and users. Domain reaches only the domains assigned to it, outbound and inbound separately (the same name on both lists is two assignments); <em>All</em> includes domains added later, and a domain user needs at least one domain or <em>All</em> on either list. The only global user cannot be demoted or deleted, and you cannot delete the account you are signed in with. A new password set here signs that user out of their other sessions.</p></div>
|
||||
<div class="sp-help-topic" id="settings"><h3>Settings</h3><p>Settings holds what is true of the whole instance. Log retention is how many days the Outbound log keeps its rows, from 7 to 365 and 90 by default; a sweep every six hours deletes older ones, and it is the main driver of how fast <code>/data</code> grows. It does not change the rotation of <code>mail.log</code>, which is daily with about two weeks kept. The sending rate limit shown under it is level 1, the per-client-IP ceiling set in <code>.env</code>: changing it needs a restart, and no level-2 limit can exceed it.</p></div>
|
||||
</div>
|
||||
<div class="box">
|
||||
<div class="sp-box-head"><span class="sp-no">05</span><h2>Account</h2></div>
|
||||
<div class="sp-help-topic" id="account"><h3>Your account</h3><p>Account is your own, whatever your role: your username and e-mail, and your password. Changing the password signs out every other session of this account and keeps the one you are using; applications have their own logins and are not affected.</p></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<footer class="sp-foot">SelfPost 1.8.0 · © 2026 Mix · <a href="#">License (AGPL-3.0)</a> · <a href="#">Source</a> · No warranty</footer>
|
||||
</div>
|
||||
</main>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,101 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en" data-theme="light">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>Delete lists.example.org · SelfPost</title>
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bulma@1.0.4/css/bulma.min.css">
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@tabler/icons-webfont@3/dist/tabler-icons.min.css">
|
||||
<link rel="stylesheet" href="../shared/brand.css">
|
||||
<link rel="stylesheet" href="theme.css">
|
||||
</head>
|
||||
<body>
|
||||
<nav class="navbar is-primary" aria-label="main navigation">
|
||||
<div class="container">
|
||||
<div class="navbar-brand"><a class="navbar-item" href="overview.html"><img src="../../selfpost-wordmark.svg" alt="SelfPost"></a></div>
|
||||
<div class="navbar-menu is-active">
|
||||
<div class="navbar-start">
|
||||
<a class="navbar-item" href="overview.html">Overview</a>
|
||||
<div class="navbar-item has-dropdown is-hoverable">
|
||||
<a class="navbar-link" href="out-domains.html">Outbound</a>
|
||||
<div class="navbar-dropdown">
|
||||
<a class="navbar-item" href="out-domains.html"><i class="ti ti-world-upload"></i>Domains</a>
|
||||
<a class="navbar-item" href="out-log.html"><i class="ti ti-list-details"></i>Log</a>
|
||||
<a class="navbar-item" href="out-queue.html"><i class="ti ti-stack-2"></i>Queue</a>
|
||||
<a class="navbar-item" href="dmarc.html"><i class="ti ti-report-analytics"></i>DMARC reports</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="navbar-item has-dropdown is-hoverable sp-current">
|
||||
<a class="navbar-link" href="in-domains.html">Inbound</a>
|
||||
<div class="navbar-dropdown">
|
||||
<a class="navbar-item is-active" href="in-domains.html"><i class="ti ti-world-download"></i>Domains</a>
|
||||
<a class="navbar-item" href="in-log.html"><i class="ti ti-list-details"></i>Log</a>
|
||||
<a class="navbar-item" href="in-queue.html"><i class="ti ti-stack-2"></i>Queue</a>
|
||||
<a class="navbar-item" href="in-quarantine.html"><i class="ti ti-mail-pause"></i>Quarantine</a>
|
||||
<a class="navbar-item" href="in-filter-lists.html"><i class="ti ti-filter"></i>Filter lists</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="navbar-item has-dropdown is-hoverable">
|
||||
<a class="navbar-link" href="health.html">Server</a>
|
||||
<div class="navbar-dropdown">
|
||||
<a class="navbar-item" href="health.html"><i class="ti ti-heartbeat"></i>Health</a>
|
||||
<a class="navbar-item" href="preflight.html"><i class="ti ti-checklist"></i>Preflight</a>
|
||||
<a class="navbar-item" href="system-log.html"><i class="ti ti-file-text"></i>System log</a>
|
||||
<a class="navbar-item" href="backup.html"><i class="ti ti-archive"></i>Backup</a>
|
||||
<a class="navbar-item" href="users.html"><i class="ti ti-users"></i>Users</a>
|
||||
<a class="navbar-item" href="settings.html"><i class="ti ti-settings"></i>Settings</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="navbar-end">
|
||||
<div class="navbar-item has-dropdown is-hoverable">
|
||||
<a class="navbar-link" href="account.html"><i class="ti ti-user-circle"></i>admin</a>
|
||||
<div class="navbar-dropdown is-right">
|
||||
<a class="navbar-item" href="account.html"><i class="ti ti-key"></i>Account</a>
|
||||
<a class="navbar-item" href="help.html"><i class="ti ti-help"></i>Help</a>
|
||||
<hr class="navbar-divider">
|
||||
<form method="post" action="login.html"><button type="submit" class="navbar-item"><i class="ti ti-logout"></i>Sign out</button></form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
<div class="sp-perf" aria-hidden="true"></div>
|
||||
<div class="sp-subnav"><div class="container"><a href="in-domains.html" aria-current="page"><i class="ti ti-world-download"></i>Domains</a><a href="in-log.html"><i class="ti ti-list-details"></i>Log</a><a href="in-queue.html"><i class="ti ti-stack-2"></i>Queue</a><a href="in-quarantine.html"><i class="ti ti-mail-pause"></i>Quarantine</a><a href="in-filter-lists.html"><i class="ti ti-filter"></i>Filter lists</a></div></div>
|
||||
<main>
|
||||
<div class="container sp-page">
|
||||
<div class="sp-head">
|
||||
<div class="sp-grow">
|
||||
<p class="sp-kicker"><a href="in-domains.html">Inbound domains</a> / <a href="in-domain.html">lists.example.org</a> /</p>
|
||||
<h1 class="title is-3">Delete lists.example.org</h1>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="columns">
|
||||
<div class="column is-7">
|
||||
<div class="box sp-danger-zone">
|
||||
<div class="sp-box-head"><span class="sp-no"><i class="ti ti-alert-triangle"></i></span><h2>This cannot be undone</h2></div>
|
||||
<div class="sp-box-body">
|
||||
<p class="mb-3">Deleting the inbound domain <strong>lists.example.org</strong> will:</p>
|
||||
<ul class="sp-list-check">
|
||||
<li><i class="ti ti-x"></i>stop accepting mail for it on port 25 — senders get a relay-denied reply;</li>
|
||||
<li><i class="ti ti-x"></i>remove its upstream and its 4 listed recipients.</li>
|
||||
</ul>
|
||||
<p class="sp-muted mb-4">Remove or repoint the MX record first, or mail for the domain will bounce. Outbound domains are not touched.</p>
|
||||
<form method="post" action="in-domains.html">
|
||||
<div class="buttons">
|
||||
<button type="submit" class="button is-danger"><i class="ti ti-trash"></i>Delete lists.example.org</button>
|
||||
<a class="button is-text" href="in-domain.html">Keep it</a>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<footer class="sp-foot">SelfPost 1.8.0 · © 2026 Mix · <a href="#">License (AGPL-3.0)</a> · <a href="#">Source</a> · No warranty</footer>
|
||||
</div>
|
||||
</main>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,171 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en" data-theme="light">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>lists.example.org · Inbound domains · SelfPost</title>
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bulma@1.0.4/css/bulma.min.css">
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@tabler/icons-webfont@3/dist/tabler-icons.min.css">
|
||||
<link rel="stylesheet" href="../shared/brand.css">
|
||||
<link rel="stylesheet" href="theme.css">
|
||||
</head>
|
||||
<body>
|
||||
<nav class="navbar is-primary" aria-label="main navigation">
|
||||
<div class="container">
|
||||
<div class="navbar-brand"><a class="navbar-item" href="overview.html"><img src="../../selfpost-wordmark.svg" alt="SelfPost"></a></div>
|
||||
<div class="navbar-menu is-active">
|
||||
<div class="navbar-start">
|
||||
<a class="navbar-item" href="overview.html">Overview</a>
|
||||
<div class="navbar-item has-dropdown is-hoverable">
|
||||
<a class="navbar-link" href="out-domains.html">Outbound</a>
|
||||
<div class="navbar-dropdown">
|
||||
<a class="navbar-item" href="out-domains.html"><i class="ti ti-world-upload"></i>Domains</a>
|
||||
<a class="navbar-item" href="out-log.html"><i class="ti ti-list-details"></i>Log</a>
|
||||
<a class="navbar-item" href="out-queue.html"><i class="ti ti-stack-2"></i>Queue</a>
|
||||
<a class="navbar-item" href="dmarc.html"><i class="ti ti-report-analytics"></i>DMARC reports</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="navbar-item has-dropdown is-hoverable sp-current">
|
||||
<a class="navbar-link" href="in-domains.html">Inbound</a>
|
||||
<div class="navbar-dropdown">
|
||||
<a class="navbar-item is-active" href="in-domains.html"><i class="ti ti-world-download"></i>Domains</a>
|
||||
<a class="navbar-item" href="in-log.html"><i class="ti ti-list-details"></i>Log</a>
|
||||
<a class="navbar-item" href="in-queue.html"><i class="ti ti-stack-2"></i>Queue</a>
|
||||
<a class="navbar-item" href="in-quarantine.html"><i class="ti ti-mail-pause"></i>Quarantine</a>
|
||||
<a class="navbar-item" href="in-filter-lists.html"><i class="ti ti-filter"></i>Filter lists</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="navbar-item has-dropdown is-hoverable">
|
||||
<a class="navbar-link" href="health.html">Server</a>
|
||||
<div class="navbar-dropdown">
|
||||
<a class="navbar-item" href="health.html"><i class="ti ti-heartbeat"></i>Health</a>
|
||||
<a class="navbar-item" href="preflight.html"><i class="ti ti-checklist"></i>Preflight</a>
|
||||
<a class="navbar-item" href="system-log.html"><i class="ti ti-file-text"></i>System log</a>
|
||||
<a class="navbar-item" href="backup.html"><i class="ti ti-archive"></i>Backup</a>
|
||||
<a class="navbar-item" href="users.html"><i class="ti ti-users"></i>Users</a>
|
||||
<a class="navbar-item" href="settings.html"><i class="ti ti-settings"></i>Settings</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="navbar-end">
|
||||
<div class="navbar-item has-dropdown is-hoverable">
|
||||
<a class="navbar-link" href="account.html"><i class="ti ti-user-circle"></i>admin</a>
|
||||
<div class="navbar-dropdown is-right">
|
||||
<a class="navbar-item" href="account.html"><i class="ti ti-key"></i>Account</a>
|
||||
<a class="navbar-item" href="help.html"><i class="ti ti-help"></i>Help</a>
|
||||
<hr class="navbar-divider">
|
||||
<form method="post" action="login.html"><button type="submit" class="navbar-item"><i class="ti ti-logout"></i>Sign out</button></form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
<div class="sp-perf" aria-hidden="true"></div>
|
||||
<div class="sp-subnav"><div class="container"><a href="in-domains.html" aria-current="page"><i class="ti ti-world-download"></i>Domains</a><a href="in-log.html"><i class="ti ti-list-details"></i>Log</a><a href="in-queue.html"><i class="ti ti-stack-2"></i>Queue</a><a href="in-quarantine.html"><i class="ti ti-mail-pause"></i>Quarantine</a><a href="in-filter-lists.html"><i class="ti ti-filter"></i>Filter lists</a></div></div>
|
||||
<main>
|
||||
<div class="container sp-page">
|
||||
<div class="sp-head">
|
||||
<div class="sp-postmark"><span>DNS</span><b>OK</b><span>MX POINTS HERE</span></div>
|
||||
<div class="sp-grow">
|
||||
<p class="sp-kicker"><a href="in-domains.html">Inbound domains</a> /</p>
|
||||
<h1 class="title is-3">lists.example.org</h1>
|
||||
<p class="sp-lead">Accepted on port 25, handed to <span class="sp-mono">mx.internal:25</span> over required TLS · 4 recipients</p>
|
||||
</div>
|
||||
<div class="buttons">
|
||||
<form method="post" action="#"><button type="submit" class="button"><i class="ti ti-refresh"></i>Re-check DNS</button></form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="columns">
|
||||
<div class="column is-3">
|
||||
<aside class="menu sp-side">
|
||||
<p class="menu-label">This domain</p>
|
||||
<ul class="menu-list">
|
||||
<li><a class="is-active" href="#dns"><i class="ti ti-world-check"></i>MX record <span class="tag is-success is-light">ok</span></a></li>
|
||||
<li><a href="#upstream"><i class="ti ti-arrow-forward-up"></i>Upstream</a></li>
|
||||
<li><a href="#recipients"><i class="ti ti-address-book"></i>Recipients <span class="tag is-light">4</span></a></li>
|
||||
<li><a href="#filter"><i class="ti ti-shield-check"></i>Spam filter <span class="tag is-success is-light">on</span></a></li>
|
||||
</ul>
|
||||
<p class="menu-label">See also</p>
|
||||
<ul class="menu-list">
|
||||
<li><a href="in-log.html"><i class="ti ti-list-details"></i>Log for this domain</a></li>
|
||||
</ul>
|
||||
<p class="menu-label">Rarely changed</p>
|
||||
<ul class="menu-list">
|
||||
<li><a class="has-text-danger" href="in-domain-delete.html"><i class="ti ti-trash has-text-danger"></i>Delete domain</a></li>
|
||||
</ul>
|
||||
</aside>
|
||||
</div>
|
||||
|
||||
<div class="column is-9">
|
||||
<div class="box" id="dns">
|
||||
<div class="sp-box-head"><span class="sp-no">01</span><h2>MX record</h2><div class="sp-end">Checked 1 min ago</div><a class="sp-help" href="help.html#inbound" title="Backup-MX and forwarding"><i class="ti ti-help-circle"></i></a></div>
|
||||
<div class="sp-record">
|
||||
<div class="sp-record-head"><h3>MX</h3><span class="tag is-success is-light">published</span><small>Keep any existing primary MX if this is a backup-MX</small></div>
|
||||
<div class="sp-row">
|
||||
<div class="field"><label class="label">Host / name</label>
|
||||
<div class="field has-addons"><div class="control is-expanded"><input class="input" readonly value="lists.example.org"></div><div class="control"><button type="button" class="button"><i class="ti ti-copy"></i>Copy</button></div></div></div>
|
||||
<div class="field"><label class="label">Type</label><div class="control"><input class="input" readonly value="MX"></div></div>
|
||||
</div>
|
||||
<div class="field"><label class="label">Value to publish</label>
|
||||
<div class="field has-addons"><div class="control is-expanded"><input class="input" readonly value="10 mail.example.org."></div><div class="control"><button type="button" class="button"><i class="ti ti-copy"></i>Copy</button></div></div></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="columns">
|
||||
<div class="column is-6">
|
||||
<div class="box" id="upstream">
|
||||
<div class="sp-box-head"><span class="sp-no">02</span><h2>Upstream</h2><div class="sp-end">Not a mailbox</div><a class="sp-help" href="help.html#upstream" title="Host, port and TLS"><i class="ti ti-help-circle"></i></a></div>
|
||||
<div class="sp-box-body">
|
||||
<form class="sp-form" method="post" action="#">
|
||||
<div class="sp-row">
|
||||
<div class="field"><label class="label" for="host">Host</label><div class="control"><input class="input sp-mono" id="host" name="host" value="mx.internal" autocomplete="off" spellcheck="false" required></div></div>
|
||||
<div class="field"><label class="label" for="port">Port</label><div class="control"><input class="input sp-mono" id="port" name="port" inputmode="numeric" value="25" required></div></div>
|
||||
</div>
|
||||
<div class="field"><label class="label" for="tls">TLS to upstream</label>
|
||||
<div class="control"><div class="select is-fullwidth"><select id="tls" name="tls_mode"><option>Opportunistic</option><option selected>Required</option><option>Off</option></select></div></div></div>
|
||||
<button type="submit" class="button is-primary">Save upstream</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="column is-6">
|
||||
<div class="box" id="recipients">
|
||||
<div class="sp-box-head"><span class="sp-no">03</span><h2>Valid recipients</h2><a class="sp-help" href="help.html#recipients" title="Listed addresses or any address"><i class="ti ti-help-circle"></i></a></div>
|
||||
<div class="sp-box-body">
|
||||
<form class="sp-form" method="post" action="#">
|
||||
<div class="field"><label class="label" for="rmode">Who to accept</label>
|
||||
<div class="control"><div class="select is-fullwidth"><select id="rmode" name="recipient_mode"><option selected>Listed addresses only</option><option>Any recipient at this domain</option></select></div></div></div>
|
||||
<div class="field"><label class="label" for="addresses">Addresses</label>
|
||||
<div class="control"><textarea class="textarea sp-mono" id="addresses" name="addresses" rows="4">announce@lists.example.org
|
||||
dev@lists.example.org
|
||||
owner@lists.example.org
|
||||
security@lists.example.org</textarea></div>
|
||||
<p class="help">Unknown recipients are rejected at RCPT, so this relay generates no backscatter. Prefer a list unless the upstream itself rejects unknowns.</p></div>
|
||||
<button type="submit" class="button is-primary">Save recipients</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="box" id="filter">
|
||||
<div class="sp-box-head"><span class="sp-no">04</span><h2>Spam filter</h2><div class="sp-end"><span class="tag is-success is-light">on</span></div><a class="sp-help" href="help.html#filter" title="A server setting"><i class="ti ti-help-circle"></i></a></div>
|
||||
<div class="sp-box-body">
|
||||
<dl class="sp-facts">
|
||||
<div><dt>Engine</dt><dd>rspamd, for every inbound domain of this server</dd></div>
|
||||
<div><dt>Suspicious mail</dt><dd>Held — <a href="in-quarantine.html">1 message in quarantine</a> for this domain</dd></div>
|
||||
<div><dt>Spam</dt><dd>Refused at the door; see <em>reject</em> in the <a href="in-log.html">log</a></dd></div>
|
||||
<div><dt>Allow and deny lists</dt><dd class="sp-muted">Kept by the global administrator, one pair for the whole server</dd></div>
|
||||
</dl>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<footer class="sp-foot">SelfPost 1.8.0 · © 2026 Mix · <a href="#">License (AGPL-3.0)</a> · <a href="#">Source</a> · No warranty</footer>
|
||||
</div>
|
||||
</main>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,105 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en" data-theme="light">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>Inbound domains · SelfPost</title>
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bulma@1.0.4/css/bulma.min.css">
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@tabler/icons-webfont@3/dist/tabler-icons.min.css">
|
||||
<link rel="stylesheet" href="../shared/brand.css">
|
||||
<link rel="stylesheet" href="theme.css">
|
||||
</head>
|
||||
<body>
|
||||
<nav class="navbar is-primary" aria-label="main navigation">
|
||||
<div class="container">
|
||||
<div class="navbar-brand"><a class="navbar-item" href="overview.html"><img src="../../selfpost-wordmark.svg" alt="SelfPost"></a></div>
|
||||
<div class="navbar-menu is-active">
|
||||
<div class="navbar-start">
|
||||
<a class="navbar-item" href="overview.html">Overview</a>
|
||||
<div class="navbar-item has-dropdown is-hoverable">
|
||||
<a class="navbar-link" href="out-domains.html">Outbound</a>
|
||||
<div class="navbar-dropdown">
|
||||
<a class="navbar-item" href="out-domains.html"><i class="ti ti-world-upload"></i>Domains</a>
|
||||
<a class="navbar-item" href="out-log.html"><i class="ti ti-list-details"></i>Log</a>
|
||||
<a class="navbar-item" href="out-queue.html"><i class="ti ti-stack-2"></i>Queue</a>
|
||||
<a class="navbar-item" href="dmarc.html"><i class="ti ti-report-analytics"></i>DMARC reports</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="navbar-item has-dropdown is-hoverable sp-current">
|
||||
<a class="navbar-link" href="in-domains.html">Inbound</a>
|
||||
<div class="navbar-dropdown">
|
||||
<a class="navbar-item is-active" href="in-domains.html"><i class="ti ti-world-download"></i>Domains</a>
|
||||
<a class="navbar-item" href="in-log.html"><i class="ti ti-list-details"></i>Log</a>
|
||||
<a class="navbar-item" href="in-queue.html"><i class="ti ti-stack-2"></i>Queue</a>
|
||||
<a class="navbar-item" href="in-quarantine.html"><i class="ti ti-mail-pause"></i>Quarantine</a>
|
||||
<a class="navbar-item" href="in-filter-lists.html"><i class="ti ti-filter"></i>Filter lists</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="navbar-item has-dropdown is-hoverable">
|
||||
<a class="navbar-link" href="health.html">Server</a>
|
||||
<div class="navbar-dropdown">
|
||||
<a class="navbar-item" href="health.html"><i class="ti ti-heartbeat"></i>Health</a>
|
||||
<a class="navbar-item" href="preflight.html"><i class="ti ti-checklist"></i>Preflight</a>
|
||||
<a class="navbar-item" href="system-log.html"><i class="ti ti-file-text"></i>System log</a>
|
||||
<a class="navbar-item" href="backup.html"><i class="ti ti-archive"></i>Backup</a>
|
||||
<a class="navbar-item" href="users.html"><i class="ti ti-users"></i>Users</a>
|
||||
<a class="navbar-item" href="settings.html"><i class="ti ti-settings"></i>Settings</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="navbar-end">
|
||||
<div class="navbar-item has-dropdown is-hoverable">
|
||||
<a class="navbar-link" href="account.html"><i class="ti ti-user-circle"></i>admin</a>
|
||||
<div class="navbar-dropdown is-right">
|
||||
<a class="navbar-item" href="account.html"><i class="ti ti-key"></i>Account</a>
|
||||
<a class="navbar-item" href="help.html"><i class="ti ti-help"></i>Help</a>
|
||||
<hr class="navbar-divider">
|
||||
<form method="post" action="login.html"><button type="submit" class="navbar-item"><i class="ti ti-logout"></i>Sign out</button></form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
<div class="sp-perf" aria-hidden="true"></div>
|
||||
<div class="sp-subnav"><div class="container"><a href="in-domains.html" aria-current="page"><i class="ti ti-world-download"></i>Domains</a><a href="in-log.html"><i class="ti ti-list-details"></i>Log</a><a href="in-queue.html"><i class="ti ti-stack-2"></i>Queue</a><a href="in-quarantine.html"><i class="ti ti-mail-pause"></i>Quarantine</a><a href="in-filter-lists.html"><i class="ti ti-filter"></i>Filter lists</a></div></div>
|
||||
<main>
|
||||
<div class="container sp-page">
|
||||
<div class="sp-head">
|
||||
<div class="sp-grow">
|
||||
<p class="sp-kicker">Inbound</p>
|
||||
<h1 class="title is-3">Inbound domains</h1>
|
||||
<p class="sp-lead">Backup-MX / forwarder. Port 25 accepts mail only for the domains listed here and hands it to their upstream. No mailboxes.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="box">
|
||||
<div class="sp-box-head"><span class="sp-no">01</span><h2>Add a domain</h2></div>
|
||||
<div class="sp-box-body">
|
||||
<form class="sp-form" method="post" action="in-domain.html">
|
||||
<div class="field has-addons">
|
||||
<div class="control is-expanded has-icons-left"><input class="input" name="name" type="text" placeholder="lists.example.com" autocomplete="off" autocapitalize="none" spellcheck="false" required><span class="icon is-left"><i class="ti ti-world"></i></span></div>
|
||||
<div class="control"><button type="submit" class="button is-primary"><i class="ti ti-plus"></i>Add domain</button></div>
|
||||
</div>
|
||||
<p class="help">Mail is not accepted until an upstream host is saved on the domain's page. Adding and deleting domains is for a global administrator; a domain administrator sees the list without this form and without Delete.</p>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="box">
|
||||
<div class="sp-box-head"><span class="sp-no">02</span><h2>Domains</h2><div class="sp-end">Spam filter <span class="tag is-success is-light">on</span> · 2 domains</div><a class="sp-help" href="help.html#inbound" title="Backup-MX and forwarding"><i class="ti ti-help-circle"></i></a></div>
|
||||
<div class="table-container"><table class="table is-fullwidth is-hoverable">
|
||||
<thead><tr><th>Domain</th><th>DNS</th><th>Upstream</th><th>Recipients</th><th>TLS to upstream</th><th></th></tr></thead>
|
||||
<tbody>
|
||||
<tr><td><a href="in-domain.html"><strong>lists.example.org</strong></a></td><td><span class="tag is-success is-light">MX</span></td><td class="sp-mono sp-muted">mx.internal:25</td><td>4 addresses</td><td><span class="tag is-success is-light">required</span></td><td class="sp-actions"><a href="in-domain.html">Open</a><a class="has-text-danger" href="in-domain-delete.html">Delete</a></td></tr>
|
||||
<tr><td><a href="in-domain.html"><strong>acme.io</strong></a></td><td><span class="tag is-success is-light">MX</span></td><td class="sp-mono sp-muted">10.0.4.12:2525</td><td>any address</td><td><span class="tag is-light">opportunistic</span></td><td class="sp-actions"><a href="in-domain.html">Open</a><a class="has-text-danger" href="in-domain-delete.html">Delete</a></td></tr>
|
||||
</tbody>
|
||||
</table></div>
|
||||
<div class="sp-box-foot">The MX tag is green when at least one MX of the domain points at this server.</div>
|
||||
</div>
|
||||
|
||||
<footer class="sp-foot">SelfPost 1.8.0 · © 2026 Mix · <a href="#">License (AGPL-3.0)</a> · <a href="#">Source</a> · No warranty</footer>
|
||||
</div>
|
||||
</main>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,145 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en" data-theme="light">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>Filter lists · SelfPost</title>
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bulma@1.0.4/css/bulma.min.css">
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@tabler/icons-webfont@3/dist/tabler-icons.min.css">
|
||||
<link rel="stylesheet" href="../shared/brand.css">
|
||||
<link rel="stylesheet" href="theme.css">
|
||||
</head>
|
||||
<body>
|
||||
<nav class="navbar is-primary" aria-label="main navigation">
|
||||
<div class="container">
|
||||
<div class="navbar-brand"><a class="navbar-item" href="overview.html"><img src="../../selfpost-wordmark.svg" alt="SelfPost"></a></div>
|
||||
<div class="navbar-menu is-active">
|
||||
<div class="navbar-start">
|
||||
<a class="navbar-item" href="overview.html">Overview</a>
|
||||
<div class="navbar-item has-dropdown is-hoverable">
|
||||
<a class="navbar-link" href="out-domains.html">Outbound</a>
|
||||
<div class="navbar-dropdown">
|
||||
<a class="navbar-item" href="out-domains.html"><i class="ti ti-world-upload"></i>Domains</a>
|
||||
<a class="navbar-item" href="out-log.html"><i class="ti ti-list-details"></i>Log</a>
|
||||
<a class="navbar-item" href="out-queue.html"><i class="ti ti-stack-2"></i>Queue</a>
|
||||
<a class="navbar-item" href="dmarc.html"><i class="ti ti-report-analytics"></i>DMARC reports</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="navbar-item has-dropdown is-hoverable sp-current">
|
||||
<a class="navbar-link" href="in-domains.html">Inbound</a>
|
||||
<div class="navbar-dropdown">
|
||||
<a class="navbar-item" href="in-domains.html"><i class="ti ti-world-download"></i>Domains</a>
|
||||
<a class="navbar-item" href="in-log.html"><i class="ti ti-list-details"></i>Log</a>
|
||||
<a class="navbar-item" href="in-queue.html"><i class="ti ti-stack-2"></i>Queue</a>
|
||||
<a class="navbar-item" href="in-quarantine.html"><i class="ti ti-mail-pause"></i>Quarantine</a>
|
||||
<a class="navbar-item is-active" href="in-filter-lists.html"><i class="ti ti-filter"></i>Filter lists</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="navbar-item has-dropdown is-hoverable">
|
||||
<a class="navbar-link" href="health.html">Server</a>
|
||||
<div class="navbar-dropdown">
|
||||
<a class="navbar-item" href="health.html"><i class="ti ti-heartbeat"></i>Health</a>
|
||||
<a class="navbar-item" href="preflight.html"><i class="ti ti-checklist"></i>Preflight</a>
|
||||
<a class="navbar-item" href="system-log.html"><i class="ti ti-file-text"></i>System log</a>
|
||||
<a class="navbar-item" href="backup.html"><i class="ti ti-archive"></i>Backup</a>
|
||||
<a class="navbar-item" href="users.html"><i class="ti ti-users"></i>Users</a>
|
||||
<a class="navbar-item" href="settings.html"><i class="ti ti-settings"></i>Settings</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="navbar-end">
|
||||
<div class="navbar-item has-dropdown is-hoverable">
|
||||
<a class="navbar-link" href="account.html"><i class="ti ti-user-circle"></i>admin</a>
|
||||
<div class="navbar-dropdown is-right">
|
||||
<a class="navbar-item" href="account.html"><i class="ti ti-key"></i>Account</a>
|
||||
<a class="navbar-item" href="help.html"><i class="ti ti-help"></i>Help</a>
|
||||
<hr class="navbar-divider">
|
||||
<form method="post" action="login.html"><button type="submit" class="navbar-item"><i class="ti ti-logout"></i>Sign out</button></form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
<div class="sp-perf" aria-hidden="true"></div>
|
||||
<div class="sp-subnav"><div class="container"><a href="in-domains.html"><i class="ti ti-world-download"></i>Domains</a><a href="in-log.html"><i class="ti ti-list-details"></i>Log</a><a href="in-queue.html"><i class="ti ti-stack-2"></i>Queue</a><a href="in-quarantine.html"><i class="ti ti-mail-pause"></i>Quarantine</a><a href="in-filter-lists.html" aria-current="page"><i class="ti ti-filter"></i>Filter lists</a></div></div>
|
||||
<main>
|
||||
<div class="container sp-page">
|
||||
<div class="sp-head">
|
||||
<div class="sp-grow">
|
||||
<p class="sp-kicker">Inbound · global role</p>
|
||||
<h1 class="title is-3">Filter lists</h1>
|
||||
<p class="sp-lead">Correct the spam filter by hand: senders it must always let through, and senders it must always turn away. One pair of lists for the whole instance.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="box">
|
||||
<div class="sp-box-head"><span class="sp-no">01</span><h2>Add an entry</h2><a class="sp-help" href="help.html#inbound" title="How the lists are applied"><i class="ti ti-help-circle"></i></a></div>
|
||||
<div class="sp-box-body">
|
||||
<form class="sp-form" method="post" action="#">
|
||||
<div class="columns">
|
||||
<div class="column is-3">
|
||||
<div class="field"><label class="label" for="list">List</label>
|
||||
<div class="control"><div class="select is-fullwidth"><select id="list" name="list_type"><option>Deny — always turn away</option><option>Allow — always let through</option></select></div></div></div>
|
||||
</div>
|
||||
<div class="column is-3">
|
||||
<div class="field"><label class="label" for="etype">Entry is</label>
|
||||
<div class="control"><div class="select is-fullwidth"><select id="etype" name="entry_type"><option>a sender address</option><option>a sender domain</option><option>a client IP or CIDR</option></select></div></div></div>
|
||||
</div>
|
||||
<div class="column is-6">
|
||||
<div class="field"><label class="label" for="value">Value</label>
|
||||
<div class="control"><input class="input sp-mono" id="value" name="value" placeholder="promo@bulk.example" autocomplete="off" autocapitalize="none" spellcheck="false" required></div></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="field"><label class="label" for="note">Note</label>
|
||||
<div class="control"><input class="input" id="note" name="note" placeholder="Why — for whoever reads this list next year"></div></div>
|
||||
<button type="submit" class="button is-primary"><i class="ti ti-plus"></i>Add entry</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="columns">
|
||||
<div class="column is-6">
|
||||
<div class="box">
|
||||
<div class="sp-box-head"><span class="sp-no">02</span><h2>Deny</h2><div class="sp-end">3 entries · wins over Allow</div></div>
|
||||
<div class="table-container"><table class="table is-fullwidth is-hoverable">
|
||||
<thead><tr><th>Value</th><th>Note</th><th>Added</th><th></th></tr></thead>
|
||||
<tbody>
|
||||
<tr><td class="sp-mono sp-small">@bulk.example <span class="tag is-light">domain</span></td><td class="sp-muted">newsletter nobody subscribed to</td><td class="sp-muted sp-nowrap sp-small">2026-09-02</td><td class="sp-actions"><a class="has-text-danger" href="#">Remove</a></td></tr>
|
||||
<tr><td class="sp-mono sp-small">198.51.100.0/24 <span class="tag is-light">ip</span></td><td class="sp-muted">dictionary attack on lists.example.org</td><td class="sp-muted sp-nowrap sp-small">2026-08-19</td><td class="sp-actions"><a class="has-text-danger" href="#">Remove</a></td></tr>
|
||||
<tr><td class="sp-mono sp-small">win@lottery.example <span class="tag is-light">address</span></td><td class="sp-muted">—</td><td class="sp-muted sp-nowrap sp-small">2026-07-30</td><td class="sp-actions"><a class="has-text-danger" href="#">Remove</a></td></tr>
|
||||
</tbody>
|
||||
</table></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="column is-6">
|
||||
<div class="box">
|
||||
<div class="sp-box-head"><span class="sp-no">03</span><h2>Allow</h2><div class="sp-end">2 entries</div></div>
|
||||
<div class="table-container"><table class="table is-fullwidth is-hoverable">
|
||||
<thead><tr><th>Value</th><th>Note</th><th>Added</th><th></th></tr></thead>
|
||||
<tbody>
|
||||
<tr><td class="sp-mono sp-small">@partner.example <span class="tag is-light">domain</span></td><td class="sp-muted">invoices were scored as spam</td><td class="sp-muted sp-nowrap sp-small">2026-09-11</td><td class="sp-actions"><a class="has-text-danger" href="#">Remove</a></td></tr>
|
||||
<tr><td class="sp-mono sp-small">noreply@github.example <span class="tag is-light">address</span></td><td class="sp-muted">security advisories</td><td class="sp-muted sp-nowrap sp-small">2026-06-04</td><td class="sp-actions"><a class="has-text-danger" href="#">Remove</a></td></tr>
|
||||
</tbody>
|
||||
</table></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="box">
|
||||
<div class="sp-box-head"><span class="sp-no">04</span><h2>Where the lists go</h2><div class="sp-end"><span class="tag is-success is-light">applied</span></div></div>
|
||||
<div class="sp-box-body">
|
||||
<dl class="sp-facts">
|
||||
<div><dt>Applied by</dt><dd>rspamd, from map files</dd></div>
|
||||
<div><dt>Files</dt><dd class="sp-mono sp-small">/data/antispam/allow.map<br>/data/antispam/deny.map</dd></div>
|
||||
<div><dt>Last written</dt><dd>2026-09-11 08:40 UTC</dd></div>
|
||||
<div><dt>When the filter is off</dt><dd class="sp-muted">The lists are kept and editable, but nothing reads them until <code>INBOUND_ANTISPAM_MILTER</code> is set.</dd></div>
|
||||
</dl>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<footer class="sp-foot">SelfPost 1.8.0 · © 2026 Mix · <a href="#">License (AGPL-3.0)</a> · <a href="#">Source</a> · No warranty</footer>
|
||||
</div>
|
||||
</main>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,105 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en" data-theme="light">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>Inbound log · SelfPost</title>
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bulma@1.0.4/css/bulma.min.css">
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@tabler/icons-webfont@3/dist/tabler-icons.min.css">
|
||||
<link rel="stylesheet" href="../shared/brand.css">
|
||||
<link rel="stylesheet" href="theme.css">
|
||||
</head>
|
||||
<body>
|
||||
<nav class="navbar is-primary" aria-label="main navigation">
|
||||
<div class="container">
|
||||
<div class="navbar-brand"><a class="navbar-item" href="overview.html"><img src="../../selfpost-wordmark.svg" alt="SelfPost"></a></div>
|
||||
<div class="navbar-menu is-active">
|
||||
<div class="navbar-start">
|
||||
<a class="navbar-item" href="overview.html">Overview</a>
|
||||
<div class="navbar-item has-dropdown is-hoverable">
|
||||
<a class="navbar-link" href="out-domains.html">Outbound</a>
|
||||
<div class="navbar-dropdown">
|
||||
<a class="navbar-item" href="out-domains.html"><i class="ti ti-world-upload"></i>Domains</a>
|
||||
<a class="navbar-item" href="out-log.html"><i class="ti ti-list-details"></i>Log</a>
|
||||
<a class="navbar-item" href="out-queue.html"><i class="ti ti-stack-2"></i>Queue</a>
|
||||
<a class="navbar-item" href="dmarc.html"><i class="ti ti-report-analytics"></i>DMARC reports</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="navbar-item has-dropdown is-hoverable sp-current">
|
||||
<a class="navbar-link" href="in-domains.html">Inbound</a>
|
||||
<div class="navbar-dropdown">
|
||||
<a class="navbar-item" href="in-domains.html"><i class="ti ti-world-download"></i>Domains</a>
|
||||
<a class="navbar-item is-active" href="in-log.html"><i class="ti ti-list-details"></i>Log</a>
|
||||
<a class="navbar-item" href="in-queue.html"><i class="ti ti-stack-2"></i>Queue</a>
|
||||
<a class="navbar-item" href="in-quarantine.html"><i class="ti ti-mail-pause"></i>Quarantine</a>
|
||||
<a class="navbar-item" href="in-filter-lists.html"><i class="ti ti-filter"></i>Filter lists</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="navbar-item has-dropdown is-hoverable">
|
||||
<a class="navbar-link" href="health.html">Server</a>
|
||||
<div class="navbar-dropdown">
|
||||
<a class="navbar-item" href="health.html"><i class="ti ti-heartbeat"></i>Health</a>
|
||||
<a class="navbar-item" href="preflight.html"><i class="ti ti-checklist"></i>Preflight</a>
|
||||
<a class="navbar-item" href="system-log.html"><i class="ti ti-file-text"></i>System log</a>
|
||||
<a class="navbar-item" href="backup.html"><i class="ti ti-archive"></i>Backup</a>
|
||||
<a class="navbar-item" href="users.html"><i class="ti ti-users"></i>Users</a>
|
||||
<a class="navbar-item" href="settings.html"><i class="ti ti-settings"></i>Settings</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="navbar-end">
|
||||
<div class="navbar-item has-dropdown is-hoverable">
|
||||
<a class="navbar-link" href="account.html"><i class="ti ti-user-circle"></i>admin</a>
|
||||
<div class="navbar-dropdown is-right">
|
||||
<a class="navbar-item" href="account.html"><i class="ti ti-key"></i>Account</a>
|
||||
<a class="navbar-item" href="help.html"><i class="ti ti-help"></i>Help</a>
|
||||
<hr class="navbar-divider">
|
||||
<form method="post" action="login.html"><button type="submit" class="navbar-item"><i class="ti ti-logout"></i>Sign out</button></form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
<div class="sp-perf" aria-hidden="true"></div>
|
||||
<div class="sp-subnav"><div class="container"><a href="in-domains.html"><i class="ti ti-world-download"></i>Domains</a><a href="in-log.html" aria-current="page"><i class="ti ti-list-details"></i>Log</a><a href="in-queue.html"><i class="ti ti-stack-2"></i>Queue</a><a href="in-quarantine.html"><i class="ti ti-mail-pause"></i>Quarantine</a><a href="in-filter-lists.html"><i class="ti ti-filter"></i>Filter lists</a></div></div>
|
||||
<main>
|
||||
<div class="container sp-page">
|
||||
<div class="sp-head">
|
||||
<div class="sp-grow">
|
||||
<p class="sp-kicker">Inbound</p>
|
||||
<h1 class="title is-3">Inbound log</h1>
|
||||
<p class="sp-lead">What port 25 decided about each message, one row per recipient. Accepts come from the inbound journal, rejects from <code>mail.log</code>; the filter's reasons appear when the spam filter is on.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="box">
|
||||
<div class="sp-box-head">
|
||||
<span class="sp-no">01</span><h2>Messages</h2>
|
||||
<form class="sp-end" method="get" action="#">
|
||||
<div class="select"><select name="domain"><option>All domains</option><option>lists.example.org</option><option>acme.io</option></select></div>
|
||||
<div class="select"><select name="decision"><option>Any decision</option><option>accept</option><option>reject</option><option>tempfail</option><option>quarantine</option></select></div>
|
||||
<input class="input" type="date" name="from" value="2026-09-14" aria-label="From date">
|
||||
<input class="input" type="date" name="to" value="2026-09-21" aria-label="To date">
|
||||
<button type="submit" class="button"><i class="ti ti-filter"></i>Filter</button>
|
||||
</form>
|
||||
</div>
|
||||
<div class="table-container"><table class="table is-fullwidth is-hoverable">
|
||||
<thead><tr><th>Time, UTC</th><th>From · client</th><th>To</th><th>Subject</th><th>Decision</th><th>Why</th><th></th></tr></thead>
|
||||
<tbody>
|
||||
<tr><td class="sp-mono sp-muted sp-nowrap sp-small">09-21 14:00:18</td><td>t.berg@<wbr>uni.example<br><span class="sp-mono sp-muted sp-small">192.0.2.77</span></td><td>dev@<wbr>lists.example.org</td><td class="sp-clip">Re: [dev] patch series v3</td><td><span class="tag is-success is-light">accept</span></td><td class="sp-muted sp-small">rspamd · score 0.4</td><td class="sp-actions"><a href="in-filter-lists.html">Deny sender</a></td></tr>
|
||||
<tr><td class="sp-mono sp-muted sp-nowrap sp-small">09-21 13:59:02</td><td>invoice@<wbr>partner-billing.example<br><span class="sp-mono sp-muted sp-small">203.0.113.140</span></td><td>owner@<wbr>lists.example.org</td><td class="sp-clip">Outstanding invoice — action required</td><td><span class="tag is-primary is-light">quarantine</span></td><td class="sp-muted sp-small">rspamd · score 9.8 · PHISHING, FORGED_SENDER</td><td class="sp-actions"><a href="in-quarantine-message.html">Review</a></td></tr>
|
||||
<tr><td class="sp-mono sp-muted sp-nowrap sp-small">09-21 13:57:41</td><td>promo@<wbr>bulk.example<br><span class="sp-mono sp-muted sp-small">198.51.100.90</span></td><td>sales@<wbr>lists.example.org</td><td class="sp-clip sp-muted">—</td><td><span class="tag is-danger is-light">reject</span></td><td class="sp-muted sp-small">postfix · 550 5.1.1 unknown recipient</td><td class="sp-actions"><a href="in-filter-lists.html">Deny sender</a></td></tr>
|
||||
<tr><td class="sp-mono sp-muted sp-nowrap sp-small">09-21 13:51:30</td><td>billing@<wbr>partner.example<br><span class="sp-mono sp-muted sp-small">198.51.100.12</span></td><td>owner@<wbr>lists.example.org</td><td class="sp-clip">Invoice 2026-0917</td><td><span class="tag is-danger is-light">reject</span></td><td class="sp-muted sp-small">rspamd · score 16.2 · BAYES_SPAM</td><td class="sp-actions"><a href="in-filter-lists.html">Allow sender</a></td></tr>
|
||||
<tr><td class="sp-mono sp-muted sp-nowrap sp-small">09-21 13:44:02</td><td>hr@<wbr>partner.example<br><span class="sp-mono sp-muted sp-small">192.0.2.31</span></td><td>jobs@<wbr>acme.io</td><td class="sp-clip">Application: backend engineer</td><td><span class="tag is-warning is-light">tempfail</span></td><td class="sp-muted sp-small">selfpost · upstream 10.0.4.12 timed out</td><td class="sp-actions"></td></tr>
|
||||
<tr><td class="sp-mono sp-muted sp-nowrap sp-small">09-21 13:30:55</td><td>noreply@<wbr>github.example<br><span class="sp-mono sp-muted sp-small">192.0.2.200</span></td><td>security@<wbr>lists.example.org</td><td class="sp-clip">[advisory] new report</td><td><span class="tag is-success is-light">accept</span></td><td class="sp-muted sp-small">rspamd · allow list</td><td class="sp-actions"></td></tr>
|
||||
</tbody>
|
||||
</table></div>
|
||||
<div class="sp-box-foot"><span>Page 1 of 38</span><a href="#">Older →</a><span class="sp-end">Allow / Deny sender opens <a href="in-filter-lists.html">Filter lists</a> with the entry filled in · global administrator only</span></div>
|
||||
</div>
|
||||
|
||||
<footer class="sp-foot">SelfPost 1.8.0 · © 2026 Mix · <a href="#">License (AGPL-3.0)</a> · <a href="#">Source</a> · No warranty</footer>
|
||||
</div>
|
||||
</main>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,149 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en" data-theme="light">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>Held message · Quarantine · SelfPost</title>
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bulma@1.0.4/css/bulma.min.css">
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@tabler/icons-webfont@3/dist/tabler-icons.min.css">
|
||||
<link rel="stylesheet" href="../shared/brand.css">
|
||||
<link rel="stylesheet" href="theme.css">
|
||||
</head>
|
||||
<body>
|
||||
<nav class="navbar is-primary" aria-label="main navigation">
|
||||
<div class="container">
|
||||
<div class="navbar-brand"><a class="navbar-item" href="overview.html"><img src="../../selfpost-wordmark.svg" alt="SelfPost"></a></div>
|
||||
<div class="navbar-menu is-active">
|
||||
<div class="navbar-start">
|
||||
<a class="navbar-item" href="overview.html">Overview</a>
|
||||
<div class="navbar-item has-dropdown is-hoverable">
|
||||
<a class="navbar-link" href="out-domains.html">Outbound</a>
|
||||
<div class="navbar-dropdown">
|
||||
<a class="navbar-item" href="out-domains.html"><i class="ti ti-world-upload"></i>Domains</a>
|
||||
<a class="navbar-item" href="out-log.html"><i class="ti ti-list-details"></i>Log</a>
|
||||
<a class="navbar-item" href="out-queue.html"><i class="ti ti-stack-2"></i>Queue</a>
|
||||
<a class="navbar-item" href="dmarc.html"><i class="ti ti-report-analytics"></i>DMARC reports</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="navbar-item has-dropdown is-hoverable sp-current">
|
||||
<a class="navbar-link" href="in-domains.html">Inbound</a>
|
||||
<div class="navbar-dropdown">
|
||||
<a class="navbar-item" href="in-domains.html"><i class="ti ti-world-download"></i>Domains</a>
|
||||
<a class="navbar-item" href="in-log.html"><i class="ti ti-list-details"></i>Log</a>
|
||||
<a class="navbar-item" href="in-queue.html"><i class="ti ti-stack-2"></i>Queue</a>
|
||||
<a class="navbar-item is-active" href="in-quarantine.html"><i class="ti ti-mail-pause"></i>Quarantine</a>
|
||||
<a class="navbar-item" href="in-filter-lists.html"><i class="ti ti-filter"></i>Filter lists</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="navbar-item has-dropdown is-hoverable">
|
||||
<a class="navbar-link" href="health.html">Server</a>
|
||||
<div class="navbar-dropdown">
|
||||
<a class="navbar-item" href="health.html"><i class="ti ti-heartbeat"></i>Health</a>
|
||||
<a class="navbar-item" href="preflight.html"><i class="ti ti-checklist"></i>Preflight</a>
|
||||
<a class="navbar-item" href="system-log.html"><i class="ti ti-file-text"></i>System log</a>
|
||||
<a class="navbar-item" href="backup.html"><i class="ti ti-archive"></i>Backup</a>
|
||||
<a class="navbar-item" href="users.html"><i class="ti ti-users"></i>Users</a>
|
||||
<a class="navbar-item" href="settings.html"><i class="ti ti-settings"></i>Settings</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="navbar-end">
|
||||
<div class="navbar-item has-dropdown is-hoverable">
|
||||
<a class="navbar-link" href="account.html"><i class="ti ti-user-circle"></i>admin</a>
|
||||
<div class="navbar-dropdown is-right">
|
||||
<a class="navbar-item" href="account.html"><i class="ti ti-key"></i>Account</a>
|
||||
<a class="navbar-item" href="help.html"><i class="ti ti-help"></i>Help</a>
|
||||
<hr class="navbar-divider">
|
||||
<form method="post" action="login.html"><button type="submit" class="navbar-item"><i class="ti ti-logout"></i>Sign out</button></form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
<div class="sp-perf" aria-hidden="true"></div>
|
||||
<div class="sp-subnav"><div class="container"><a href="in-domains.html"><i class="ti ti-world-download"></i>Domains</a><a href="in-log.html"><i class="ti ti-list-details"></i>Log</a><a href="in-queue.html"><i class="ti ti-stack-2"></i>Queue</a><a href="in-quarantine.html" aria-current="page"><i class="ti ti-mail-pause"></i>Quarantine</a><a href="in-filter-lists.html"><i class="ti ti-filter"></i>Filter lists</a></div></div>
|
||||
<main>
|
||||
<div class="container sp-page">
|
||||
<div class="sp-head">
|
||||
<div class="sp-grow">
|
||||
<p class="sp-kicker"><a href="in-quarantine.html">Quarantine</a> /</p>
|
||||
<h1 class="title is-4">Outstanding invoice — action required</h1>
|
||||
<p class="sp-route"><span>invoice@partner-billing.example</span><i class="ti ti-arrow-right sp-muted"></i><span>owner@lists.example.org</span><span class="tag is-primary is-light">quarantine</span></p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="columns">
|
||||
<div class="column is-7">
|
||||
<div class="box">
|
||||
<div class="sp-box-head"><span class="sp-no">01</span><h2>Message</h2></div>
|
||||
<div class="sp-box-body">
|
||||
<dl class="sp-facts">
|
||||
<div><dt>Inbound domain</dt><dd><a href="in-domain.html">lists.example.org</a></dd></div>
|
||||
<div><dt>Client</dt><dd class="sp-mono">203.0.113.140</dd></div>
|
||||
<div><dt>Held</dt><dd>2026-09-21 13:59:02 UTC</dd></div>
|
||||
<div><dt>Deleted on</dt><dd>2026-10-05 · in 14 days</dd></div>
|
||||
<div><dt>Size</dt><dd>48 kB · 1 attachment (<span class="sp-mono">invoice.html</span>)</dd></div>
|
||||
<div><dt>Would go to</dt><dd class="sp-mono">mx.internal:25</dd></div>
|
||||
</dl>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="column is-5">
|
||||
<div class="box">
|
||||
<div class="sp-box-head"><span class="sp-no">02</span><h2>Why it was held</h2><div class="sp-end">rspamd · score 9.8</div></div>
|
||||
<div class="table-container"><table class="table is-fullwidth">
|
||||
<thead><tr><th>Symbol</th><th>Score</th></tr></thead>
|
||||
<tbody>
|
||||
<tr><td class="sp-mono sp-small">PHISHING</td><td>5.0</td></tr>
|
||||
<tr><td class="sp-mono sp-small">FORGED_SENDER</td><td>3.0</td></tr>
|
||||
<tr><td class="sp-mono sp-small">HTML_ATTACHMENT</td><td>1.8</td></tr>
|
||||
</tbody>
|
||||
</table></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="box">
|
||||
<div class="sp-box-head"><span class="sp-no">03</span><h2>Headers</h2><div class="sp-end">The body and attachments are never rendered in the panel</div></div>
|
||||
<pre class="sp-log"><span class="sp-d">Received:</span> from mail.partner-billing.example (unknown [203.0.113.140]) by mail.example.org
|
||||
<span class="sp-d">From:</span> "Partner Billing" <invoice@partner-billing.example>
|
||||
<span class="sp-d">Reply-To:</span> <span class="sp-w">payments.dept@freemail.example</span>
|
||||
<span class="sp-d">To:</span> owner@lists.example.org
|
||||
<span class="sp-d">Subject:</span> Outstanding invoice — action required
|
||||
<span class="sp-d">Date:</span> Mon, 21 Sep 2026 13:58:57 +0000
|
||||
<span class="sp-d">Authentication-Results:</span> <span class="sp-e">spf=fail dkim=none dmarc=fail</span></pre>
|
||||
</div>
|
||||
|
||||
<div class="columns">
|
||||
<div class="column is-6">
|
||||
<div class="box">
|
||||
<div class="sp-box-head"><span class="sp-no">04</span><h2>Release</h2></div>
|
||||
<div class="sp-box-body">
|
||||
<p class="mb-3">Hands the message to <span class="sp-mono">mx.internal:25</span> unchanged, as if it had just arrived. It leaves the quarantine.</p>
|
||||
<form class="sp-form" method="post" action="in-quarantine.html">
|
||||
<div class="field"><label class="checkbox"><input type="checkbox" name="allow_sender"> Also add <span class="sp-mono">invoice@partner-billing.example</span> to the Allow list</label></div>
|
||||
<button type="submit" class="button is-primary"><i class="ti ti-send"></i>Release to upstream</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="column is-6">
|
||||
<div class="box sp-danger-zone">
|
||||
<div class="sp-box-head"><span class="sp-no"><i class="ti ti-alert-triangle"></i></span><h2>Discard</h2></div>
|
||||
<div class="sp-box-body">
|
||||
<p class="mb-3">Deletes the message now instead of in 14 days. The sender is not told. This cannot be undone.</p>
|
||||
<form class="sp-form" method="post" action="in-quarantine.html">
|
||||
<div class="field"><label class="checkbox"><input type="checkbox" name="deny_sender" checked> Also add <span class="sp-mono">@partner-billing.example</span> to the Deny list</label></div>
|
||||
<button type="submit" class="button is-danger"><i class="ti ti-trash"></i>Discard message</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<footer class="sp-foot">SelfPost 1.8.0 · © 2026 Mix · <a href="#">License (AGPL-3.0)</a> · <a href="#">Source</a> · No warranty</footer>
|
||||
</div>
|
||||
</main>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,99 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en" data-theme="light">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>Quarantine · SelfPost</title>
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bulma@1.0.4/css/bulma.min.css">
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@tabler/icons-webfont@3/dist/tabler-icons.min.css">
|
||||
<link rel="stylesheet" href="../shared/brand.css">
|
||||
<link rel="stylesheet" href="theme.css">
|
||||
</head>
|
||||
<body>
|
||||
<nav class="navbar is-primary" aria-label="main navigation">
|
||||
<div class="container">
|
||||
<div class="navbar-brand"><a class="navbar-item" href="overview.html"><img src="../../selfpost-wordmark.svg" alt="SelfPost"></a></div>
|
||||
<div class="navbar-menu is-active">
|
||||
<div class="navbar-start">
|
||||
<a class="navbar-item" href="overview.html">Overview</a>
|
||||
<div class="navbar-item has-dropdown is-hoverable">
|
||||
<a class="navbar-link" href="out-domains.html">Outbound</a>
|
||||
<div class="navbar-dropdown">
|
||||
<a class="navbar-item" href="out-domains.html"><i class="ti ti-world-upload"></i>Domains</a>
|
||||
<a class="navbar-item" href="out-log.html"><i class="ti ti-list-details"></i>Log</a>
|
||||
<a class="navbar-item" href="out-queue.html"><i class="ti ti-stack-2"></i>Queue</a>
|
||||
<a class="navbar-item" href="dmarc.html"><i class="ti ti-report-analytics"></i>DMARC reports</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="navbar-item has-dropdown is-hoverable sp-current">
|
||||
<a class="navbar-link" href="in-domains.html">Inbound</a>
|
||||
<div class="navbar-dropdown">
|
||||
<a class="navbar-item" href="in-domains.html"><i class="ti ti-world-download"></i>Domains</a>
|
||||
<a class="navbar-item" href="in-log.html"><i class="ti ti-list-details"></i>Log</a>
|
||||
<a class="navbar-item" href="in-queue.html"><i class="ti ti-stack-2"></i>Queue</a>
|
||||
<a class="navbar-item is-active" href="in-quarantine.html"><i class="ti ti-mail-pause"></i>Quarantine</a>
|
||||
<a class="navbar-item" href="in-filter-lists.html"><i class="ti ti-filter"></i>Filter lists</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="navbar-item has-dropdown is-hoverable">
|
||||
<a class="navbar-link" href="health.html">Server</a>
|
||||
<div class="navbar-dropdown">
|
||||
<a class="navbar-item" href="health.html"><i class="ti ti-heartbeat"></i>Health</a>
|
||||
<a class="navbar-item" href="preflight.html"><i class="ti ti-checklist"></i>Preflight</a>
|
||||
<a class="navbar-item" href="system-log.html"><i class="ti ti-file-text"></i>System log</a>
|
||||
<a class="navbar-item" href="backup.html"><i class="ti ti-archive"></i>Backup</a>
|
||||
<a class="navbar-item" href="users.html"><i class="ti ti-users"></i>Users</a>
|
||||
<a class="navbar-item" href="settings.html"><i class="ti ti-settings"></i>Settings</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="navbar-end">
|
||||
<div class="navbar-item has-dropdown is-hoverable">
|
||||
<a class="navbar-link" href="account.html"><i class="ti ti-user-circle"></i>admin</a>
|
||||
<div class="navbar-dropdown is-right">
|
||||
<a class="navbar-item" href="account.html"><i class="ti ti-key"></i>Account</a>
|
||||
<a class="navbar-item" href="help.html"><i class="ti ti-help"></i>Help</a>
|
||||
<hr class="navbar-divider">
|
||||
<form method="post" action="login.html"><button type="submit" class="navbar-item"><i class="ti ti-logout"></i>Sign out</button></form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
<div class="sp-perf" aria-hidden="true"></div>
|
||||
<div class="sp-subnav"><div class="container"><a href="in-domains.html"><i class="ti ti-world-download"></i>Domains</a><a href="in-log.html"><i class="ti ti-list-details"></i>Log</a><a href="in-queue.html"><i class="ti ti-stack-2"></i>Queue</a><a href="in-quarantine.html" aria-current="page"><i class="ti ti-mail-pause"></i>Quarantine</a><a href="in-filter-lists.html"><i class="ti ti-filter"></i>Filter lists</a></div></div>
|
||||
<main>
|
||||
<div class="container sp-page">
|
||||
<div class="sp-head">
|
||||
<div class="sp-grow">
|
||||
<p class="sp-kicker">Inbound</p>
|
||||
<h1 class="title is-3">Quarantine</h1>
|
||||
<p class="sp-lead">Mail the filter found suspicious enough to hold, but not to refuse. Release hands a message to its upstream as if it had just arrived; Discard deletes it. Anything left is deleted after 14 days.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="box">
|
||||
<div class="sp-box-head">
|
||||
<span class="sp-no">01</span><h2>Held messages</h2>
|
||||
<form class="sp-end" method="get" action="#">
|
||||
<div class="select"><select name="domain"><option>All domains</option><option>lists.example.org</option><option>acme.io</option></select></div>
|
||||
<button type="submit" class="button"><i class="ti ti-filter"></i>Filter</button>
|
||||
</form>
|
||||
</div>
|
||||
<div class="table-container"><table class="table is-fullwidth is-hoverable">
|
||||
<thead><tr><th>Held, UTC</th><th>From</th><th>To</th><th>Subject</th><th>Why</th><th>Size</th><th>Deleted in</th><th></th></tr></thead>
|
||||
<tbody>
|
||||
<tr><td class="sp-mono sp-muted sp-nowrap sp-small">09-21 13:59:02</td><td>invoice@<wbr>partner-billing.example</td><td>owner@<wbr>lists.example.org</td><td class="sp-clip"><a href="in-quarantine-message.html"><strong>Outstanding invoice — action required</strong></a></td><td class="sp-muted sp-small">score 9.8 · PHISHING</td><td class="sp-muted sp-nowrap">48 kB</td><td class="sp-nowrap">14 days</td><td class="sp-actions"><a href="in-quarantine-message.html">Review</a></td></tr>
|
||||
<tr><td class="sp-mono sp-muted sp-nowrap sp-small">09-20 22:14:40</td><td>no-reply@<wbr>shipping-update.example</td><td>jobs@<wbr>acme.io</td><td class="sp-clip"><a href="in-quarantine-message.html"><strong>Your parcel could not be delivered</strong></a></td><td class="sp-muted sp-small">score 8.1 · URL_SHORTENER</td><td class="sp-muted sp-nowrap">12 kB</td><td class="sp-nowrap">13 days</td><td class="sp-actions"><a href="in-quarantine-message.html">Review</a></td></tr>
|
||||
<tr><td class="sp-mono sp-muted sp-nowrap sp-small">09-09 07:02:11</td><td>m.ortega@<wbr>supplier.example</td><td>owner@<wbr>lists.example.org</td><td class="sp-clip"><a href="in-quarantine-message.html"><strong>Fwd: scanned contract (3 attachments)</strong></a></td><td class="sp-muted sp-small">score 7.4 · MIME_ARCHIVE</td><td class="sp-muted sp-nowrap">2.9 MB</td><td class="sp-nowrap"><span class="tag is-warning is-light">2 days</span></td><td class="sp-actions"><a href="in-quarantine-message.html">Review</a></td></tr>
|
||||
</tbody>
|
||||
</table></div>
|
||||
<div class="sp-box-foot"><span>3 messages · 3.0 MB of 500 MB</span><span class="sp-end">A domain administrator sees only their assigned inbound domains</span></div>
|
||||
</div>
|
||||
|
||||
<footer class="sp-foot">SelfPost 1.8.0 · © 2026 Mix · <a href="#">License (AGPL-3.0)</a> · <a href="#">Source</a> · No warranty</footer>
|
||||
</div>
|
||||
</main>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,97 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en" data-theme="light">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>Inbound queue · SelfPost</title>
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bulma@1.0.4/css/bulma.min.css">
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@tabler/icons-webfont@3/dist/tabler-icons.min.css">
|
||||
<link rel="stylesheet" href="../shared/brand.css">
|
||||
<link rel="stylesheet" href="theme.css">
|
||||
</head>
|
||||
<body>
|
||||
<nav class="navbar is-primary" aria-label="main navigation">
|
||||
<div class="container">
|
||||
<div class="navbar-brand"><a class="navbar-item" href="overview.html"><img src="../../selfpost-wordmark.svg" alt="SelfPost"></a></div>
|
||||
<div class="navbar-menu is-active">
|
||||
<div class="navbar-start">
|
||||
<a class="navbar-item" href="overview.html">Overview</a>
|
||||
<div class="navbar-item has-dropdown is-hoverable">
|
||||
<a class="navbar-link" href="out-domains.html">Outbound</a>
|
||||
<div class="navbar-dropdown">
|
||||
<a class="navbar-item" href="out-domains.html"><i class="ti ti-world-upload"></i>Domains</a>
|
||||
<a class="navbar-item" href="out-log.html"><i class="ti ti-list-details"></i>Log</a>
|
||||
<a class="navbar-item" href="out-queue.html"><i class="ti ti-stack-2"></i>Queue</a>
|
||||
<a class="navbar-item" href="dmarc.html"><i class="ti ti-report-analytics"></i>DMARC reports</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="navbar-item has-dropdown is-hoverable sp-current">
|
||||
<a class="navbar-link" href="in-domains.html">Inbound</a>
|
||||
<div class="navbar-dropdown">
|
||||
<a class="navbar-item" href="in-domains.html"><i class="ti ti-world-download"></i>Domains</a>
|
||||
<a class="navbar-item" href="in-log.html"><i class="ti ti-list-details"></i>Log</a>
|
||||
<a class="navbar-item is-active" href="in-queue.html"><i class="ti ti-stack-2"></i>Queue</a>
|
||||
<a class="navbar-item" href="in-quarantine.html"><i class="ti ti-mail-pause"></i>Quarantine</a>
|
||||
<a class="navbar-item" href="in-filter-lists.html"><i class="ti ti-filter"></i>Filter lists</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="navbar-item has-dropdown is-hoverable">
|
||||
<a class="navbar-link" href="health.html">Server</a>
|
||||
<div class="navbar-dropdown">
|
||||
<a class="navbar-item" href="health.html"><i class="ti ti-heartbeat"></i>Health</a>
|
||||
<a class="navbar-item" href="preflight.html"><i class="ti ti-checklist"></i>Preflight</a>
|
||||
<a class="navbar-item" href="system-log.html"><i class="ti ti-file-text"></i>System log</a>
|
||||
<a class="navbar-item" href="backup.html"><i class="ti ti-archive"></i>Backup</a>
|
||||
<a class="navbar-item" href="users.html"><i class="ti ti-users"></i>Users</a>
|
||||
<a class="navbar-item" href="settings.html"><i class="ti ti-settings"></i>Settings</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="navbar-end">
|
||||
<div class="navbar-item has-dropdown is-hoverable">
|
||||
<a class="navbar-link" href="account.html"><i class="ti ti-user-circle"></i>admin</a>
|
||||
<div class="navbar-dropdown is-right">
|
||||
<a class="navbar-item" href="account.html"><i class="ti ti-key"></i>Account</a>
|
||||
<a class="navbar-item" href="help.html"><i class="ti ti-help"></i>Help</a>
|
||||
<hr class="navbar-divider">
|
||||
<form method="post" action="login.html"><button type="submit" class="navbar-item"><i class="ti ti-logout"></i>Sign out</button></form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
<div class="sp-perf" aria-hidden="true"></div>
|
||||
<div class="sp-subnav"><div class="container"><a href="in-domains.html"><i class="ti ti-world-download"></i>Domains</a><a href="in-log.html"><i class="ti ti-list-details"></i>Log</a><a href="in-queue.html" aria-current="page"><i class="ti ti-stack-2"></i>Queue</a><a href="in-quarantine.html"><i class="ti ti-mail-pause"></i>Quarantine</a><a href="in-filter-lists.html"><i class="ti ti-filter"></i>Filter lists</a></div></div>
|
||||
<main>
|
||||
<div class="container sp-page">
|
||||
<div class="sp-head">
|
||||
<div class="sp-grow">
|
||||
<p class="sp-kicker">Inbound</p>
|
||||
<h1 class="title is-3">Inbound queue</h1>
|
||||
<p class="sp-lead">Mail accepted on port 25 and still waiting for its upstream. The same Postfix queue as <a href="out-queue.html">outbound</a>, filtered by recipient domain.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="box">
|
||||
<div class="sp-box-head"><span class="sp-no">01</span><h2>Waiting</h2><div class="sp-end">Refreshes every 10 s</div></div>
|
||||
<div class="sp-empty"><i class="ti ti-stack-2"></i>Nothing is waiting. Every accepted message has reached its upstream.</div>
|
||||
</div>
|
||||
|
||||
<div class="box">
|
||||
<div class="sp-box-head"><span class="sp-no">02</span><h2>How retries work</h2><div class="sp-end">This Postfix's policy, read at panel start</div></div>
|
||||
<div class="sp-box-body">
|
||||
<dl class="sp-facts">
|
||||
<div><dt>First retry</dt><dd class="sp-big">5 min</dd></div>
|
||||
<div><dt>Later retries</dt><dd class="sp-big">doubling</dd><dd class="sp-muted">capped at 66 min</dd></div>
|
||||
<div><dt>Kept in queue</dt><dd class="sp-big">5 days</dd></div>
|
||||
<div><dt>Then</dt><dd class="sp-big">bounced</dd><dd class="sp-muted">back to the original sender</dd></div>
|
||||
</dl>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<footer class="sp-foot">SelfPost 1.8.0 · © 2026 Mix · <a href="#">License (AGPL-3.0)</a> · <a href="#">Source</a> · No warranty</footer>
|
||||
</div>
|
||||
</main>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,31 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en" data-theme="light">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>Sign in · SelfPost</title>
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bulma@1.0.4/css/bulma.min.css">
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@tabler/icons-webfont@3/dist/tabler-icons.min.css">
|
||||
<link rel="stylesheet" href="../shared/brand.css">
|
||||
<link rel="stylesheet" href="theme.css">
|
||||
</head>
|
||||
<body>
|
||||
<div class="columns is-gapless sp-signin">
|
||||
<div class="column is-half sp-signin-brand">
|
||||
<div>
|
||||
<img src="../../selfpost-stamp.svg" alt="SelfPost">
|
||||
<p>YOUR SMTP. YOUR RULES.</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="column is-half sp-signin-form">
|
||||
<div class="sp-form">
|
||||
<p class="sp-kicker">mail.example.org</p>
|
||||
<h1 class="title is-3">No administrator yet</h1>
|
||||
<p class="sp-muted">Open the one-time setup link printed in the container log to create one.</p>
|
||||
</div>
|
||||
<p class="sp-foot">© 2026 Mix · <a href="#">License (AGPL-3.0)</a> · <a href="#">Source</a> · No warranty</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,39 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en" data-theme="light">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>Sign in · SelfPost</title>
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bulma@1.0.4/css/bulma.min.css">
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@tabler/icons-webfont@3/dist/tabler-icons.min.css">
|
||||
<link rel="stylesheet" href="../shared/brand.css">
|
||||
<link rel="stylesheet" href="theme.css">
|
||||
</head>
|
||||
<body>
|
||||
<div class="columns is-gapless sp-signin">
|
||||
<div class="column is-half sp-signin-brand">
|
||||
<div>
|
||||
<img src="../../selfpost-stamp.svg" alt="SelfPost">
|
||||
<p>YOUR SMTP. YOUR RULES.</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="column is-half sp-signin-form">
|
||||
<form class="sp-form" method="post" action="overview.html">
|
||||
<p class="sp-kicker">mail.example.org</p>
|
||||
<h1 class="title is-3">Sign in</h1>
|
||||
<div class="field">
|
||||
<label class="label" for="username">Username</label>
|
||||
<div class="control has-icons-left"><input class="input" id="username" name="username" autocomplete="username" required><span class="icon is-left"><i class="ti ti-user"></i></span></div>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label class="label" for="password">Password</label>
|
||||
<div class="control has-icons-left"><input class="input" id="password" name="password" type="password" autocomplete="current-password" required><span class="icon is-left"><i class="ti ti-lock"></i></span></div>
|
||||
</div>
|
||||
<div class="field mt-5"><button type="submit" class="button is-primary is-fullwidth">Sign in</button></div>
|
||||
</form>
|
||||
<p class="sp-foot">© 2026 Mix · <a href="#">License (AGPL-3.0)</a> · <a href="#">Source</a> · No warranty</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,107 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en" data-theme="light">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>New password · prod-server · SelfPost</title>
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bulma@1.0.4/css/bulma.min.css">
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@tabler/icons-webfont@3/dist/tabler-icons.min.css">
|
||||
<link rel="stylesheet" href="../shared/brand.css">
|
||||
<link rel="stylesheet" href="theme.css">
|
||||
</head>
|
||||
<body>
|
||||
<nav class="navbar is-primary" aria-label="main navigation">
|
||||
<div class="container">
|
||||
<div class="navbar-brand"><a class="navbar-item" href="overview.html"><img src="../../selfpost-wordmark.svg" alt="SelfPost"></a></div>
|
||||
<div class="navbar-menu is-active">
|
||||
<div class="navbar-start">
|
||||
<a class="navbar-item" href="overview.html">Overview</a>
|
||||
<div class="navbar-item has-dropdown is-hoverable sp-current">
|
||||
<a class="navbar-link" href="out-domains.html">Outbound</a>
|
||||
<div class="navbar-dropdown">
|
||||
<a class="navbar-item is-active" href="out-domains.html"><i class="ti ti-world-upload"></i>Domains</a>
|
||||
<a class="navbar-item" href="out-log.html"><i class="ti ti-list-details"></i>Log</a>
|
||||
<a class="navbar-item" href="out-queue.html"><i class="ti ti-stack-2"></i>Queue</a>
|
||||
<a class="navbar-item" href="dmarc.html"><i class="ti ti-report-analytics"></i>DMARC reports</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="navbar-item has-dropdown is-hoverable">
|
||||
<a class="navbar-link" href="in-domains.html">Inbound</a>
|
||||
<div class="navbar-dropdown">
|
||||
<a class="navbar-item" href="in-domains.html"><i class="ti ti-world-download"></i>Domains</a>
|
||||
<a class="navbar-item" href="in-log.html"><i class="ti ti-list-details"></i>Log</a>
|
||||
<a class="navbar-item" href="in-queue.html"><i class="ti ti-stack-2"></i>Queue</a>
|
||||
<a class="navbar-item" href="in-quarantine.html"><i class="ti ti-mail-pause"></i>Quarantine</a>
|
||||
<a class="navbar-item" href="in-filter-lists.html"><i class="ti ti-filter"></i>Filter lists</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="navbar-item has-dropdown is-hoverable">
|
||||
<a class="navbar-link" href="health.html">Server</a>
|
||||
<div class="navbar-dropdown">
|
||||
<a class="navbar-item" href="health.html"><i class="ti ti-heartbeat"></i>Health</a>
|
||||
<a class="navbar-item" href="preflight.html"><i class="ti ti-checklist"></i>Preflight</a>
|
||||
<a class="navbar-item" href="system-log.html"><i class="ti ti-file-text"></i>System log</a>
|
||||
<a class="navbar-item" href="backup.html"><i class="ti ti-archive"></i>Backup</a>
|
||||
<a class="navbar-item" href="users.html"><i class="ti ti-users"></i>Users</a>
|
||||
<a class="navbar-item" href="settings.html"><i class="ti ti-settings"></i>Settings</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="navbar-end">
|
||||
<div class="navbar-item has-dropdown is-hoverable">
|
||||
<a class="navbar-link" href="account.html"><i class="ti ti-user-circle"></i>admin</a>
|
||||
<div class="navbar-dropdown is-right">
|
||||
<a class="navbar-item" href="account.html"><i class="ti ti-key"></i>Account</a>
|
||||
<a class="navbar-item" href="help.html"><i class="ti ti-help"></i>Help</a>
|
||||
<hr class="navbar-divider">
|
||||
<form method="post" action="login.html"><button type="submit" class="navbar-item"><i class="ti ti-logout"></i>Sign out</button></form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
<div class="sp-perf" aria-hidden="true"></div>
|
||||
<div class="sp-subnav"><div class="container"><a href="out-domains.html" aria-current="page"><i class="ti ti-world-upload"></i>Domains</a><a href="out-log.html"><i class="ti ti-list-details"></i>Log</a><a href="out-queue.html"><i class="ti ti-stack-2"></i>Queue</a><a href="dmarc.html"><i class="ti ti-report-analytics"></i>DMARC reports</a></div></div>
|
||||
<main>
|
||||
<div class="container sp-page">
|
||||
<div class="sp-head">
|
||||
<div class="sp-grow">
|
||||
<p class="sp-kicker"><a href="out-domains.html">Outbound domains</a> / <a href="out-domain.html">example.org</a> / Applications /</p>
|
||||
<h1 class="title is-3">New password for <span class="sp-mono">prod-server</span></h1>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="columns">
|
||||
<div class="column is-7">
|
||||
<div class="box sp-credential">
|
||||
<div class="sp-box-head"><span class="sp-no"><i class="ti ti-eye"></i></span><h2>Shown once</h2><div class="sp-end">Not stored — copy it now</div></div>
|
||||
<div class="sp-record">
|
||||
<div class="field"><label class="label">Login</label>
|
||||
<div class="field has-addons"><div class="control is-expanded"><input class="input" readonly value="prod-server"></div><div class="control"><button type="button" class="button"><i class="ti ti-copy"></i>Copy</button></div></div></div>
|
||||
<div class="field"><label class="label">Password</label>
|
||||
<div class="field has-addons"><div class="control is-expanded"><input class="input" readonly value="kT7v-Qm2x-Lp9d-Ue4s-Hn6b-Rz3w"></div><div class="control"><button type="button" class="button"><i class="ti ti-copy"></i>Copy</button></div></div>
|
||||
<p class="help">The previous password stopped working when this one was generated. If this one is lost, generate another.</p></div>
|
||||
</div>
|
||||
<div class="sp-box-foot"><a class="button is-primary" href="out-domain.html">Done — back to example.org</a></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="column is-5">
|
||||
<div class="box">
|
||||
<div class="sp-box-head"><span class="sp-no"><i class="ti ti-plug"></i></span><h2>Connect with</h2></div>
|
||||
<div class="sp-box-body">
|
||||
<dl class="sp-facts">
|
||||
<div><dt>Server</dt><dd class="sp-mono">mail.example.org</dd></div>
|
||||
<div><dt>Port</dt><dd>465 SSL/TLS · 587 STARTTLS</dd></div>
|
||||
<div><dt>May send as</dt><dd class="sp-mono">*@example.org</dd></div>
|
||||
</dl>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<footer class="sp-foot">SelfPost 1.8.0 · © 2026 Mix · <a href="#">License (AGPL-3.0)</a> · <a href="#">Source</a> · No warranty</footer>
|
||||
</div>
|
||||
</main>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,136 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en" data-theme="light">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>prod-server · example.org · SelfPost</title>
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bulma@1.0.4/css/bulma.min.css">
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@tabler/icons-webfont@3/dist/tabler-icons.min.css">
|
||||
<link rel="stylesheet" href="../shared/brand.css">
|
||||
<link rel="stylesheet" href="theme.css">
|
||||
</head>
|
||||
<body>
|
||||
<nav class="navbar is-primary" aria-label="main navigation">
|
||||
<div class="container">
|
||||
<div class="navbar-brand"><a class="navbar-item" href="overview.html"><img src="../../selfpost-wordmark.svg" alt="SelfPost"></a></div>
|
||||
<div class="navbar-menu is-active">
|
||||
<div class="navbar-start">
|
||||
<a class="navbar-item" href="overview.html">Overview</a>
|
||||
<div class="navbar-item has-dropdown is-hoverable sp-current">
|
||||
<a class="navbar-link" href="out-domains.html">Outbound</a>
|
||||
<div class="navbar-dropdown">
|
||||
<a class="navbar-item is-active" href="out-domains.html"><i class="ti ti-world-upload"></i>Domains</a>
|
||||
<a class="navbar-item" href="out-log.html"><i class="ti ti-list-details"></i>Log</a>
|
||||
<a class="navbar-item" href="out-queue.html"><i class="ti ti-stack-2"></i>Queue</a>
|
||||
<a class="navbar-item" href="dmarc.html"><i class="ti ti-report-analytics"></i>DMARC reports</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="navbar-item has-dropdown is-hoverable">
|
||||
<a class="navbar-link" href="in-domains.html">Inbound</a>
|
||||
<div class="navbar-dropdown">
|
||||
<a class="navbar-item" href="in-domains.html"><i class="ti ti-world-download"></i>Domains</a>
|
||||
<a class="navbar-item" href="in-log.html"><i class="ti ti-list-details"></i>Log</a>
|
||||
<a class="navbar-item" href="in-queue.html"><i class="ti ti-stack-2"></i>Queue</a>
|
||||
<a class="navbar-item" href="in-quarantine.html"><i class="ti ti-mail-pause"></i>Quarantine</a>
|
||||
<a class="navbar-item" href="in-filter-lists.html"><i class="ti ti-filter"></i>Filter lists</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="navbar-item has-dropdown is-hoverable">
|
||||
<a class="navbar-link" href="health.html">Server</a>
|
||||
<div class="navbar-dropdown">
|
||||
<a class="navbar-item" href="health.html"><i class="ti ti-heartbeat"></i>Health</a>
|
||||
<a class="navbar-item" href="preflight.html"><i class="ti ti-checklist"></i>Preflight</a>
|
||||
<a class="navbar-item" href="system-log.html"><i class="ti ti-file-text"></i>System log</a>
|
||||
<a class="navbar-item" href="backup.html"><i class="ti ti-archive"></i>Backup</a>
|
||||
<a class="navbar-item" href="users.html"><i class="ti ti-users"></i>Users</a>
|
||||
<a class="navbar-item" href="settings.html"><i class="ti ti-settings"></i>Settings</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="navbar-end">
|
||||
<div class="navbar-item has-dropdown is-hoverable">
|
||||
<a class="navbar-link" href="account.html"><i class="ti ti-user-circle"></i>admin</a>
|
||||
<div class="navbar-dropdown is-right">
|
||||
<a class="navbar-item" href="account.html"><i class="ti ti-key"></i>Account</a>
|
||||
<a class="navbar-item" href="help.html"><i class="ti ti-help"></i>Help</a>
|
||||
<hr class="navbar-divider">
|
||||
<form method="post" action="login.html"><button type="submit" class="navbar-item"><i class="ti ti-logout"></i>Sign out</button></form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
<div class="sp-perf" aria-hidden="true"></div>
|
||||
<div class="sp-subnav"><div class="container"><a href="out-domains.html" aria-current="page"><i class="ti ti-world-upload"></i>Domains</a><a href="out-log.html"><i class="ti ti-list-details"></i>Log</a><a href="out-queue.html"><i class="ti ti-stack-2"></i>Queue</a><a href="dmarc.html"><i class="ti ti-report-analytics"></i>DMARC reports</a></div></div>
|
||||
<main>
|
||||
<div class="container sp-page">
|
||||
<div class="sp-head">
|
||||
<div class="sp-grow">
|
||||
<p class="sp-kicker"><a href="out-domains.html">Outbound domains</a> / <a href="out-domain.html">example.org</a> / Applications /</p>
|
||||
<h1 class="title is-3 sp-mono">prod-server</h1>
|
||||
<p class="sp-lead">1 102 messages in 30 days · peak 96 msg/h · average 1.5 msg/h. The same form, empty, adds a new application.</p>
|
||||
</div>
|
||||
<div class="buttons">
|
||||
<a class="button" href="out-app-created.html"><i class="ti ti-key"></i>New password</a>
|
||||
<a class="button is-danger is-light" href="#"><i class="ti ti-trash"></i>Delete</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<form class="sp-form" method="post" action="out-domain.html">
|
||||
<div class="columns">
|
||||
<div class="column is-4">
|
||||
<div class="box">
|
||||
<div class="sp-box-head"><span class="sp-no">01</span><h2>Sender</h2></div>
|
||||
<div class="sp-box-body">
|
||||
<div class="field"><label class="label" for="login">Login</label>
|
||||
<div class="control"><input class="input sp-mono" id="login" name="login" value="prod-server" autocomplete="off" spellcheck="false" required></div>
|
||||
<p class="help">Unique across domains; letters, digits, <code>.</code> <code>-</code> <code>_</code>.</p></div>
|
||||
<div class="field"><label class="label" for="amode">May send as</label>
|
||||
<div class="control"><div class="select is-fullwidth"><select id="amode" name="mode"><option selected>Any address of the domain</option><option>Specific addresses (list)</option></select></div></div></div>
|
||||
<div class="field"><label class="label" for="addresses">Addresses</label>
|
||||
<div class="control"><textarea class="textarea" id="addresses" name="addresses" rows="3" placeholder="alerts@example.org" disabled></textarea></div>
|
||||
<p class="help">One per line. Used only with a list.</p></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="column is-4">
|
||||
<div class="box">
|
||||
<div class="sp-box-head"><span class="sp-no">02</span><h2>Client IP allow-list</h2><div class="sp-end"><span class="tag is-light">off</span></div><a class="sp-help" href="help.html#apps" title="What an application is"><i class="ti ti-help-circle"></i></a></div>
|
||||
<div class="sp-box-body">
|
||||
<div class="field"><label class="checkbox"><input type="checkbox" name="auth_ip_restrict"> Restrict to listed IPs</label></div>
|
||||
<div class="field"><label class="label" for="ips">Allowed client IPs</label>
|
||||
<div class="control"><textarea class="textarea sp-mono" id="ips" name="auth_allowed_ips" rows="3" placeholder="203.0.113.10"></textarea></div>
|
||||
<p class="help">A login from anywhere else is refused with a 4xx at <code>MAIL FROM</code>. A sending control, fail-open — not an authentication boundary.</p></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="column is-4">
|
||||
<div class="box">
|
||||
<div class="sp-box-head"><span class="sp-no">03</span><h2>Rate limit</h2><div class="sp-end"><span class="tag is-success is-light">active</span></div><a class="sp-help" href="help.html#limits" title="Levels 1 and 2"><i class="ti ti-help-circle"></i></a></div>
|
||||
<div class="sp-box-body">
|
||||
<div class="field"><label class="label" for="rmode">Limit mode</label>
|
||||
<div class="control"><div class="select is-fullwidth"><select id="rmode" name="rl_mode"><option>Use the domain limit</option><option selected>Manual</option><option>Auto (from statistics)</option></select></div></div></div>
|
||||
<div class="sp-pair">
|
||||
<div class="field"><label class="label" for="max">Messages (max 600)</label><div class="control"><input class="input" id="max" name="max_messages" type="number" min="1" max="600" value="200"></div></div>
|
||||
<div class="field"><label class="label" for="win">Window, s</label><div class="control"><input class="input" id="win" name="window_seconds" type="number" min="1" value="3600"></div></div>
|
||||
</div>
|
||||
<p class="help">Overrides the domain limit (288 / h) for this application; may be higher or lower, never above level 1.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="buttons">
|
||||
<button type="submit" class="button is-primary">Save application</button>
|
||||
<a class="button is-text" href="out-domain.html">Cancel</a>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<footer class="sp-foot">SelfPost 1.8.0 · © 2026 Mix · <a href="#">License (AGPL-3.0)</a> · <a href="#">Source</a> · No warranty</footer>
|
||||
</div>
|
||||
</main>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,110 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en" data-theme="light">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>Delete example.org · SelfPost</title>
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bulma@1.0.4/css/bulma.min.css">
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@tabler/icons-webfont@3/dist/tabler-icons.min.css">
|
||||
<link rel="stylesheet" href="../shared/brand.css">
|
||||
<link rel="stylesheet" href="theme.css">
|
||||
</head>
|
||||
<body>
|
||||
<nav class="navbar is-primary" aria-label="main navigation">
|
||||
<div class="container">
|
||||
<div class="navbar-brand"><a class="navbar-item" href="overview.html"><img src="../../selfpost-wordmark.svg" alt="SelfPost"></a></div>
|
||||
<div class="navbar-menu is-active">
|
||||
<div class="navbar-start">
|
||||
<a class="navbar-item" href="overview.html">Overview</a>
|
||||
<div class="navbar-item has-dropdown is-hoverable sp-current">
|
||||
<a class="navbar-link" href="out-domains.html">Outbound</a>
|
||||
<div class="navbar-dropdown">
|
||||
<a class="navbar-item is-active" href="out-domains.html"><i class="ti ti-world-upload"></i>Domains</a>
|
||||
<a class="navbar-item" href="out-log.html"><i class="ti ti-list-details"></i>Log</a>
|
||||
<a class="navbar-item" href="out-queue.html"><i class="ti ti-stack-2"></i>Queue</a>
|
||||
<a class="navbar-item" href="dmarc.html"><i class="ti ti-report-analytics"></i>DMARC reports</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="navbar-item has-dropdown is-hoverable">
|
||||
<a class="navbar-link" href="in-domains.html">Inbound</a>
|
||||
<div class="navbar-dropdown">
|
||||
<a class="navbar-item" href="in-domains.html"><i class="ti ti-world-download"></i>Domains</a>
|
||||
<a class="navbar-item" href="in-log.html"><i class="ti ti-list-details"></i>Log</a>
|
||||
<a class="navbar-item" href="in-queue.html"><i class="ti ti-stack-2"></i>Queue</a>
|
||||
<a class="navbar-item" href="in-quarantine.html"><i class="ti ti-mail-pause"></i>Quarantine</a>
|
||||
<a class="navbar-item" href="in-filter-lists.html"><i class="ti ti-filter"></i>Filter lists</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="navbar-item has-dropdown is-hoverable">
|
||||
<a class="navbar-link" href="health.html">Server</a>
|
||||
<div class="navbar-dropdown">
|
||||
<a class="navbar-item" href="health.html"><i class="ti ti-heartbeat"></i>Health</a>
|
||||
<a class="navbar-item" href="preflight.html"><i class="ti ti-checklist"></i>Preflight</a>
|
||||
<a class="navbar-item" href="system-log.html"><i class="ti ti-file-text"></i>System log</a>
|
||||
<a class="navbar-item" href="backup.html"><i class="ti ti-archive"></i>Backup</a>
|
||||
<a class="navbar-item" href="users.html"><i class="ti ti-users"></i>Users</a>
|
||||
<a class="navbar-item" href="settings.html"><i class="ti ti-settings"></i>Settings</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="navbar-end">
|
||||
<div class="navbar-item has-dropdown is-hoverable">
|
||||
<a class="navbar-link" href="account.html"><i class="ti ti-user-circle"></i>admin</a>
|
||||
<div class="navbar-dropdown is-right">
|
||||
<a class="navbar-item" href="account.html"><i class="ti ti-key"></i>Account</a>
|
||||
<a class="navbar-item" href="help.html"><i class="ti ti-help"></i>Help</a>
|
||||
<hr class="navbar-divider">
|
||||
<form method="post" action="login.html"><button type="submit" class="navbar-item"><i class="ti ti-logout"></i>Sign out</button></form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
<div class="sp-perf" aria-hidden="true"></div>
|
||||
<div class="sp-subnav"><div class="container"><a href="out-domains.html" aria-current="page"><i class="ti ti-world-upload"></i>Domains</a><a href="out-log.html"><i class="ti ti-list-details"></i>Log</a><a href="out-queue.html"><i class="ti ti-stack-2"></i>Queue</a><a href="dmarc.html"><i class="ti ti-report-analytics"></i>DMARC reports</a></div></div>
|
||||
<main>
|
||||
<div class="container sp-page">
|
||||
<div class="sp-head">
|
||||
<div class="sp-grow">
|
||||
<p class="sp-kicker"><a href="out-domains.html">Outbound domains</a> / <a href="out-domain.html">example.org</a> /</p>
|
||||
<h1 class="title is-3">Delete example.org</h1>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="columns">
|
||||
<div class="column is-7">
|
||||
<div class="box sp-danger-zone">
|
||||
<div class="sp-box-head"><span class="sp-no"><i class="ti ti-alert-triangle"></i></span><h2>This cannot be undone</h2></div>
|
||||
<div class="sp-box-body">
|
||||
<p class="mb-3">Deleting <strong>example.org</strong> will:</p>
|
||||
<ul class="sp-list-check">
|
||||
<li><i class="ti ti-x"></i>permanently delete its DKIM signing key;</li>
|
||||
<li><i class="ti ti-x"></i>delete <strong>both applications</strong> (<span class="sp-mono">prod-server</span>, <span class="sp-mono">alerts</span>) with their credentials and sender bindings;</li>
|
||||
<li><i class="ti ti-x"></i>reload OpenDKIM so the domain is no longer signed.</li>
|
||||
</ul>
|
||||
<p class="sp-muted mb-4">The published DKIM record becomes invalid — remove it from DNS unless you plan to re-add the domain. Inbound domains are not touched.</p>
|
||||
<form method="post" action="out-domains.html">
|
||||
<div class="buttons">
|
||||
<button type="submit" class="button is-danger"><i class="ti ti-trash"></i>Delete example.org</button>
|
||||
<a class="button is-text" href="out-domain.html">Keep it</a>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="column is-5">
|
||||
<div class="box">
|
||||
<div class="sp-box-head"><span class="sp-no"><i class="ti ti-file-export"></i></span><h2>Moving it instead?</h2></div>
|
||||
<div class="sp-box-body">
|
||||
<p><a href="out-domain-settings.html">Export the domain</a> first. The file carries the key and the application passwords, so the new instance needs no DNS change.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<footer class="sp-foot">SelfPost 1.8.0 · © 2026 Mix · <a href="#">License (AGPL-3.0)</a> · <a href="#">Source</a> · No warranty</footer>
|
||||
</div>
|
||||
</main>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,147 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en" data-theme="light">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>Settings · example.org · SelfPost</title>
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bulma@1.0.4/css/bulma.min.css">
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@tabler/icons-webfont@3/dist/tabler-icons.min.css">
|
||||
<link rel="stylesheet" href="../shared/brand.css">
|
||||
<link rel="stylesheet" href="theme.css">
|
||||
</head>
|
||||
<body>
|
||||
<nav class="navbar is-primary" aria-label="main navigation">
|
||||
<div class="container">
|
||||
<div class="navbar-brand"><a class="navbar-item" href="overview.html"><img src="../../selfpost-wordmark.svg" alt="SelfPost"></a></div>
|
||||
<div class="navbar-menu is-active">
|
||||
<div class="navbar-start">
|
||||
<a class="navbar-item" href="overview.html">Overview</a>
|
||||
<div class="navbar-item has-dropdown is-hoverable sp-current">
|
||||
<a class="navbar-link" href="out-domains.html">Outbound</a>
|
||||
<div class="navbar-dropdown">
|
||||
<a class="navbar-item is-active" href="out-domains.html"><i class="ti ti-world-upload"></i>Domains</a>
|
||||
<a class="navbar-item" href="out-log.html"><i class="ti ti-list-details"></i>Log</a>
|
||||
<a class="navbar-item" href="out-queue.html"><i class="ti ti-stack-2"></i>Queue</a>
|
||||
<a class="navbar-item" href="dmarc.html"><i class="ti ti-report-analytics"></i>DMARC reports</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="navbar-item has-dropdown is-hoverable">
|
||||
<a class="navbar-link" href="in-domains.html">Inbound</a>
|
||||
<div class="navbar-dropdown">
|
||||
<a class="navbar-item" href="in-domains.html"><i class="ti ti-world-download"></i>Domains</a>
|
||||
<a class="navbar-item" href="in-log.html"><i class="ti ti-list-details"></i>Log</a>
|
||||
<a class="navbar-item" href="in-queue.html"><i class="ti ti-stack-2"></i>Queue</a>
|
||||
<a class="navbar-item" href="in-quarantine.html"><i class="ti ti-mail-pause"></i>Quarantine</a>
|
||||
<a class="navbar-item" href="in-filter-lists.html"><i class="ti ti-filter"></i>Filter lists</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="navbar-item has-dropdown is-hoverable">
|
||||
<a class="navbar-link" href="health.html">Server</a>
|
||||
<div class="navbar-dropdown">
|
||||
<a class="navbar-item" href="health.html"><i class="ti ti-heartbeat"></i>Health</a>
|
||||
<a class="navbar-item" href="preflight.html"><i class="ti ti-checklist"></i>Preflight</a>
|
||||
<a class="navbar-item" href="system-log.html"><i class="ti ti-file-text"></i>System log</a>
|
||||
<a class="navbar-item" href="backup.html"><i class="ti ti-archive"></i>Backup</a>
|
||||
<a class="navbar-item" href="users.html"><i class="ti ti-users"></i>Users</a>
|
||||
<a class="navbar-item" href="settings.html"><i class="ti ti-settings"></i>Settings</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="navbar-end">
|
||||
<div class="navbar-item has-dropdown is-hoverable">
|
||||
<a class="navbar-link" href="account.html"><i class="ti ti-user-circle"></i>admin</a>
|
||||
<div class="navbar-dropdown is-right">
|
||||
<a class="navbar-item" href="account.html"><i class="ti ti-key"></i>Account</a>
|
||||
<a class="navbar-item" href="help.html"><i class="ti ti-help"></i>Help</a>
|
||||
<hr class="navbar-divider">
|
||||
<form method="post" action="login.html"><button type="submit" class="navbar-item"><i class="ti ti-logout"></i>Sign out</button></form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
<div class="sp-perf" aria-hidden="true"></div>
|
||||
<div class="sp-subnav"><div class="container"><a href="out-domains.html" aria-current="page"><i class="ti ti-world-upload"></i>Domains</a><a href="out-log.html"><i class="ti ti-list-details"></i>Log</a><a href="out-queue.html"><i class="ti ti-stack-2"></i>Queue</a><a href="dmarc.html"><i class="ti ti-report-analytics"></i>DMARC reports</a></div></div>
|
||||
<main>
|
||||
<div class="container sp-page">
|
||||
<div class="sp-head">
|
||||
<div class="sp-grow">
|
||||
<p class="sp-kicker"><a href="out-domains.html">Outbound domains</a> / <a href="out-domain.html">example.org</a> /</p>
|
||||
<h1 class="title is-3">Domain settings</h1>
|
||||
<p class="sp-lead">The things set once and rarely touched: where DMARC reports go, the sending ceiling, moving the domain elsewhere.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="columns">
|
||||
<div class="column is-6">
|
||||
<div class="box" id="reports">
|
||||
<div class="sp-box-head"><span class="sp-no">01</span><h2>DMARC report address</h2><a class="sp-help" href="help.html#dmarc" title="Where DMARC reports go"><i class="ti ti-help-circle"></i></a></div>
|
||||
<div class="sp-box-body">
|
||||
<form class="sp-form" method="post" action="#">
|
||||
<div class="field"><label class="label" for="rua">Aggregate reports (rua=)</label>
|
||||
<div class="control"><div class="select is-fullwidth"><select id="rua" name="dmarc_rua_mode">
|
||||
<option>No reports</option>
|
||||
<option>SelfPost hosted — example.org@dmarc.mail.example.org</option>
|
||||
<option selected>A specific address</option>
|
||||
</select></div></div>
|
||||
<p class="help">No reports leaves <code>rua=</code> out of the DMARC record. SelfPost hosted sends them to an address on this server, which parses them for DMARC reports. A specific address is any mailbox, and it receives the raw XML. Changing this changes the DMARC record to publish.</p></div>
|
||||
<div class="field"><label class="label" for="rua_email">Report address</label>
|
||||
<div class="field has-addons"><div class="control is-expanded"><input class="input" id="rua_email" name="dmarc_rua_email" type="email" value="reports@acme.io" placeholder="reports@your-mail-domain.com" autocomplete="off" spellcheck="false"></div><div class="control"><button type="button" class="button" data-fill="rua_email" data-fill-value="mix@example.org"><i class="ti ti-mail"></i>Use my e-mail</button></div></div></div>
|
||||
<button type="submit" class="button is-primary">Save report address</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="box" id="export">
|
||||
<div class="sp-box-head"><span class="sp-no">03</span><h2>Export domain</h2><a class="sp-help" href="help.html#backup" title="Moving a domain to another instance"><i class="ti ti-help-circle"></i></a></div>
|
||||
<div class="sp-box-body">
|
||||
<p class="mb-3">One file with the DKIM key and every application's password hash — import it on another SelfPost and the published DNS needs no change. <strong>The file is a secret.</strong></p>
|
||||
<form class="sp-form" method="post" action="#">
|
||||
<div class="field"><label class="checkbox"><input type="checkbox" name="encrypt" checked> Encrypt with a password (<code>.spde</code>)</label></div>
|
||||
<div class="sp-pair">
|
||||
<div class="field"><label class="label" for="encpw">Password</label><div class="control"><input class="input" id="encpw" type="password" placeholder="at least 12 characters"></div></div>
|
||||
<div class="field"><label class="label" for="encpw2">Repeat password</label><div class="control"><input class="input" id="encpw2" type="password"></div></div>
|
||||
</div>
|
||||
<p class="help mb-3">Keep this password: SelfPost does not store it, and the file cannot be opened without it.</p>
|
||||
<button type="submit" class="button"><i class="ti ti-download"></i>Export domain</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="column is-6">
|
||||
<div class="box" id="limit">
|
||||
<div class="sp-box-head"><span class="sp-no">02</span><h2>Rate limit</h2><div class="sp-end"><span class="tag is-success is-light">active</span></div><a class="sp-help" href="help.html#limits" title="Levels 1 and 2"><i class="ti ti-help-circle"></i></a></div>
|
||||
<div class="sp-box-body">
|
||||
<form class="sp-form" method="post" action="#">
|
||||
<div class="field"><label class="label" for="mode">Limit mode</label>
|
||||
<div class="control"><div class="select is-fullwidth"><select id="mode" name="mode"><option>Manual</option><option selected>Auto (from statistics)</option></select></div></div></div>
|
||||
<div class="field"><label class="label" for="mult">Multiplier (1.5–10, default 3)</label>
|
||||
<div class="control"><input class="input" id="mult" name="auto_multiplier" type="number" min="1.5" max="10" step="0.1" value="3"></div>
|
||||
<p class="help">Computed limit: <strong>288</strong> messages / 3600 s. Peak was 96 msg/h. Last recalculated yesterday 03:00 UTC.</p></div>
|
||||
<div class="buttons">
|
||||
<button type="submit" class="button is-primary">Save limit</button>
|
||||
<button type="submit" class="button" formaction="#recalc">Recalculate now</button>
|
||||
<button type="submit" class="button is-danger is-light" formaction="#clear">Remove limit</button>
|
||||
</div>
|
||||
</form>
|
||||
<p class="sp-muted mt-4">Applies to every sender on this domain and cannot exceed level 1 (600 messages / 3600 s per client IP, set in <code>.env</code>). An application may carry its own limit above or below this one.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="box sp-danger-zone">
|
||||
<div class="sp-box-head"><span class="sp-no">04</span><h2>Delete domain</h2></div>
|
||||
<div class="sp-box-body">
|
||||
<p class="mb-3">Removes the DKIM key and both applications. Asks once more before doing it.</p>
|
||||
<a class="button is-danger is-light" href="out-domain-delete.html"><i class="ti ti-trash"></i>Delete example.org…</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<footer class="sp-foot">SelfPost 1.8.0 · © 2026 Mix · <a href="#">License (AGPL-3.0)</a> · <a href="#">Source</a> · No warranty</footer>
|
||||
</div>
|
||||
</main>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,179 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en" data-theme="light">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>example.org · Outbound domains · SelfPost</title>
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bulma@1.0.4/css/bulma.min.css">
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@tabler/icons-webfont@3/dist/tabler-icons.min.css">
|
||||
<link rel="stylesheet" href="../shared/brand.css">
|
||||
<link rel="stylesheet" href="theme.css">
|
||||
</head>
|
||||
<body>
|
||||
<nav class="navbar is-primary" aria-label="main navigation">
|
||||
<div class="container">
|
||||
<div class="navbar-brand"><a class="navbar-item" href="overview.html"><img src="../../selfpost-wordmark.svg" alt="SelfPost"></a></div>
|
||||
<div class="navbar-menu is-active">
|
||||
<div class="navbar-start">
|
||||
<a class="navbar-item" href="overview.html">Overview</a>
|
||||
<div class="navbar-item has-dropdown is-hoverable sp-current">
|
||||
<a class="navbar-link" href="out-domains.html">Outbound</a>
|
||||
<div class="navbar-dropdown">
|
||||
<a class="navbar-item is-active" href="out-domains.html"><i class="ti ti-world-upload"></i>Domains</a>
|
||||
<a class="navbar-item" href="out-log.html"><i class="ti ti-list-details"></i>Log</a>
|
||||
<a class="navbar-item" href="out-queue.html"><i class="ti ti-stack-2"></i>Queue</a>
|
||||
<a class="navbar-item" href="dmarc.html"><i class="ti ti-report-analytics"></i>DMARC reports</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="navbar-item has-dropdown is-hoverable">
|
||||
<a class="navbar-link" href="in-domains.html">Inbound</a>
|
||||
<div class="navbar-dropdown">
|
||||
<a class="navbar-item" href="in-domains.html"><i class="ti ti-world-download"></i>Domains</a>
|
||||
<a class="navbar-item" href="in-log.html"><i class="ti ti-list-details"></i>Log</a>
|
||||
<a class="navbar-item" href="in-queue.html"><i class="ti ti-stack-2"></i>Queue</a>
|
||||
<a class="navbar-item" href="in-quarantine.html"><i class="ti ti-mail-pause"></i>Quarantine</a>
|
||||
<a class="navbar-item" href="in-filter-lists.html"><i class="ti ti-filter"></i>Filter lists</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="navbar-item has-dropdown is-hoverable">
|
||||
<a class="navbar-link" href="health.html">Server</a>
|
||||
<div class="navbar-dropdown">
|
||||
<a class="navbar-item" href="health.html"><i class="ti ti-heartbeat"></i>Health</a>
|
||||
<a class="navbar-item" href="preflight.html"><i class="ti ti-checklist"></i>Preflight</a>
|
||||
<a class="navbar-item" href="system-log.html"><i class="ti ti-file-text"></i>System log</a>
|
||||
<a class="navbar-item" href="backup.html"><i class="ti ti-archive"></i>Backup</a>
|
||||
<a class="navbar-item" href="users.html"><i class="ti ti-users"></i>Users</a>
|
||||
<a class="navbar-item" href="settings.html"><i class="ti ti-settings"></i>Settings</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="navbar-end">
|
||||
<div class="navbar-item has-dropdown is-hoverable">
|
||||
<a class="navbar-link" href="account.html"><i class="ti ti-user-circle"></i>admin</a>
|
||||
<div class="navbar-dropdown is-right">
|
||||
<a class="navbar-item" href="account.html"><i class="ti ti-key"></i>Account</a>
|
||||
<a class="navbar-item" href="help.html"><i class="ti ti-help"></i>Help</a>
|
||||
<hr class="navbar-divider">
|
||||
<form method="post" action="login.html"><button type="submit" class="navbar-item"><i class="ti ti-logout"></i>Sign out</button></form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
<div class="sp-perf" aria-hidden="true"></div>
|
||||
<div class="sp-subnav"><div class="container"><a href="out-domains.html" aria-current="page"><i class="ti ti-world-upload"></i>Domains</a><a href="out-log.html"><i class="ti ti-list-details"></i>Log</a><a href="out-queue.html"><i class="ti ti-stack-2"></i>Queue</a><a href="dmarc.html"><i class="ti ti-report-analytics"></i>DMARC reports</a></div></div>
|
||||
<main>
|
||||
<div class="container sp-page">
|
||||
<div class="sp-head">
|
||||
<div class="sp-postmark sp-fail"><span>DNS</span><b>FAIL</b><span>1 OF 3 RECORDS</span></div>
|
||||
<div class="sp-grow">
|
||||
<p class="sp-kicker"><a href="out-domains.html">Outbound domains</a> /</p>
|
||||
<h1 class="title is-3">example.org</h1>
|
||||
<p class="sp-lead">1 284 messages in 30 days · peak 96 msg/h · average 1.8 msg/h</p>
|
||||
</div>
|
||||
<div class="buttons">
|
||||
<form method="post" action="#"><button type="submit" class="button"><i class="ti ti-refresh"></i>Re-check DNS</button></form>
|
||||
<a class="button is-primary" href="out-app.html"><i class="ti ti-plus"></i>Add application</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="columns">
|
||||
<div class="column is-3">
|
||||
<aside class="menu sp-side">
|
||||
<p class="menu-label">This domain</p>
|
||||
<ul class="menu-list">
|
||||
<li><a class="is-active" href="#dns"><i class="ti ti-world-check"></i>DNS records <span class="tag is-danger is-light">1</span></a></li>
|
||||
<li><a href="#apps"><i class="ti ti-apps"></i>Applications <span class="tag is-light">2</span></a></li>
|
||||
<li><a href="#connection"><i class="ti ti-plug"></i>Connection</a></li>
|
||||
</ul>
|
||||
<p class="menu-label">See also</p>
|
||||
<ul class="menu-list">
|
||||
<li><a href="out-log.html"><i class="ti ti-list-details"></i>Log for this domain</a></li>
|
||||
<li><a href="dmarc-domain.html"><i class="ti ti-report-analytics"></i>DMARC reports</a></li>
|
||||
</ul>
|
||||
<p class="menu-label">Rarely changed</p>
|
||||
<ul class="menu-list">
|
||||
<li><a href="out-domain-settings.html"><i class="ti ti-mail-forward"></i>Report address</a></li>
|
||||
<li><a href="out-domain-settings.html"><i class="ti ti-gauge"></i>Rate limit <span class="tag is-success is-light">auto</span></a></li>
|
||||
<li><a href="out-domain-settings.html"><i class="ti ti-file-export"></i>Export</a></li>
|
||||
<li><a class="has-text-danger" href="out-domain-delete.html"><i class="ti ti-trash has-text-danger"></i>Delete domain</a></li>
|
||||
</ul>
|
||||
</aside>
|
||||
</div>
|
||||
|
||||
<div class="column is-9">
|
||||
<div class="box" id="dns">
|
||||
<div class="sp-box-head"><span class="sp-no">01</span><h2>DNS records</h2><div class="sp-end">Checked 3 min ago</div><a class="sp-help" href="help.html#dns" title="What to publish and why"><i class="ti ti-help-circle"></i></a></div>
|
||||
|
||||
<div class="sp-record">
|
||||
<div class="sp-record-head"><h3>DKIM</h3><span class="tag is-success is-light">published</span><small>Not a secret · selector <code>sp2026</code></small></div>
|
||||
<div class="sp-row">
|
||||
<div class="field"><label class="label">Host / name</label>
|
||||
<div class="field has-addons"><div class="control is-expanded"><input class="input" readonly value="sp2026._domainkey.example.org"></div><div class="control"><button type="button" class="button"><i class="ti ti-copy"></i>Copy</button></div></div></div>
|
||||
<div class="field"><label class="label">Type</label><div class="control"><input class="input" readonly value="TXT"></div></div>
|
||||
</div>
|
||||
<div class="field"><label class="label">Value to publish</label>
|
||||
<div class="field has-addons"><div class="control is-expanded"><textarea class="textarea" rows="3" readonly>v=DKIM1; k=rsa; p=MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAx3kqV0mJd8e2f1Yb7Qw5uT9sLr4nHc6pZaXo1vKe0yWm2gBt8RjN5dCq3hUf7lPs9aEi4oMz6xTb1kGv0nYw…IDAQAB</textarea></div><div class="control"><button type="button" class="button"><i class="ti ti-copy"></i>Copy</button></div></div></div>
|
||||
</div>
|
||||
|
||||
<div class="sp-record">
|
||||
<div class="sp-record-head"><h3>SPF</h3><span class="tag is-danger is-light">mismatch</span><small>Merge into the existing record — never publish a second one</small></div>
|
||||
<div class="sp-row">
|
||||
<div class="field"><label class="label">Host / name</label>
|
||||
<div class="field has-addons"><div class="control is-expanded"><input class="input" readonly value="example.org"></div><div class="control"><button type="button" class="button"><i class="ti ti-copy"></i>Copy</button></div></div></div>
|
||||
<div class="field"><label class="label">Type</label><div class="control"><input class="input" readonly value="TXT"></div></div>
|
||||
</div>
|
||||
<div class="field"><label class="label">Value to publish</label>
|
||||
<div class="field has-addons"><div class="control is-expanded"><input class="input" readonly value="v=spf1 ip4:203.0.113.25 ~all"></div><div class="control"><button type="button" class="button"><i class="ti ti-copy"></i>Copy</button></div></div></div>
|
||||
<div class="field"><label class="label">In DNS now</label>
|
||||
<div class="control"><input class="input is-danger" readonly value="v=spf1 include:_spf.google.com ~all"></div>
|
||||
<p class="help is-danger">The published record does not authorize 203.0.113.25. Add <code>ip4:203.0.113.25</code> before <code>~all</code>. Shallow check: <code>include:</code> is not followed.</p></div>
|
||||
</div>
|
||||
|
||||
<div class="sp-record">
|
||||
<div class="sp-record-head"><h3>DMARC</h3><span class="tag is-success is-light">published</span><small>Report address: <a href="out-domain-settings.html">example.org@dmarc.mail.example.org</a></small></div>
|
||||
<div class="sp-row">
|
||||
<div class="field"><label class="label">Host / name</label>
|
||||
<div class="field has-addons"><div class="control is-expanded"><input class="input" readonly value="_dmarc.example.org"></div><div class="control"><button type="button" class="button"><i class="ti ti-copy"></i>Copy</button></div></div></div>
|
||||
<div class="field"><label class="label">Type</label><div class="control"><input class="input" readonly value="TXT"></div></div>
|
||||
</div>
|
||||
<div class="field"><label class="label">Value to publish</label>
|
||||
<div class="field has-addons"><div class="control is-expanded"><input class="input" readonly value="v=DMARC1; p=none; rua=mailto:example.org@dmarc.mail.example.org"></div><div class="control"><button type="button" class="button"><i class="ti ti-copy"></i>Copy</button></div></div>
|
||||
<p class="help"><code>p=none</code> does not affect delivery. Tighten to <code>quarantine</code>, then <code>reject</code>, once the <a href="dmarc-domain.html">reports</a> look clean.</p></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="box" id="apps">
|
||||
<div class="sp-box-head"><span class="sp-no">02</span><h2>Applications</h2><div class="sp-end">SASL logins that may send as this domain</div><a class="sp-help" href="help.html#apps" title="What an application is"><i class="ti ti-help-circle"></i></a></div>
|
||||
<div class="table-container"><table class="table is-fullwidth is-hoverable">
|
||||
<thead><tr><th>Login</th><th>May send as</th><th>30 days</th><th>Limits</th><th></th></tr></thead>
|
||||
<tbody>
|
||||
<tr><td class="sp-mono sp-nowrap"><strong>prod-server</strong></td><td>*@example.org</td><td class="sp-muted">1 102 msg · peak 96/h</td><td><span class="tag is-success is-light">200 / h</span></td>
|
||||
<td class="sp-actions"><a href="out-app.html">Edit</a><form method="post" action="out-app-created.html"><button type="submit">New password</button></form><form method="post" action="out-domain.html"><button type="submit" class="has-text-danger">Delete</button></form></td></tr>
|
||||
<tr><td class="sp-mono sp-nowrap"><strong>alerts</strong></td><td>alerts@example.org, noc@example.org</td><td class="sp-muted">182 msg · peak 14/h</td><td><span class="tag is-light">domain</span> <span class="tag is-success is-light">2 IPs</span></td>
|
||||
<td class="sp-actions"><a href="out-app.html">Edit</a><form method="post" action="out-app-created.html"><button type="submit">New password</button></form><form method="post" action="out-domain.html"><button type="submit" class="has-text-danger">Delete</button></form></td></tr>
|
||||
</tbody>
|
||||
</table></div>
|
||||
<div class="sp-box-foot">New password and Delete ask for confirmation; the old password stops working at once.</div>
|
||||
</div>
|
||||
|
||||
<div class="box" id="connection">
|
||||
<div class="sp-box-head"><span class="sp-no">03</span><h2>Connection</h2><div class="sp-end">The same for every domain</div></div>
|
||||
<div class="sp-box-body">
|
||||
<dl class="sp-facts">
|
||||
<div><dt>Server</dt><dd class="sp-mono">mail.example.org</dd></div>
|
||||
<div><dt>Port 465</dt><dd>SSL/TLS (implicit)</dd></div>
|
||||
<div><dt>Port 587</dt><dd>STARTTLS (submission)</dd></div>
|
||||
<div><dt>Authentication</dt><dd>Application login, required on every port</dd></div>
|
||||
</dl>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<footer class="sp-foot">SelfPost 1.8.0 · © 2026 Mix · <a href="#">License (AGPL-3.0)</a> · <a href="#">Source</a> · No warranty</footer>
|
||||
</div>
|
||||
</main>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,107 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en" data-theme="light">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>Outbound domains · SelfPost</title>
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bulma@1.0.4/css/bulma.min.css">
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@tabler/icons-webfont@3/dist/tabler-icons.min.css">
|
||||
<link rel="stylesheet" href="../shared/brand.css">
|
||||
<link rel="stylesheet" href="theme.css">
|
||||
</head>
|
||||
<body>
|
||||
<nav class="navbar is-primary" aria-label="main navigation">
|
||||
<div class="container">
|
||||
<div class="navbar-brand"><a class="navbar-item" href="overview.html"><img src="../../selfpost-wordmark.svg" alt="SelfPost"></a></div>
|
||||
<div class="navbar-menu is-active">
|
||||
<div class="navbar-start">
|
||||
<a class="navbar-item" href="overview.html">Overview</a>
|
||||
<div class="navbar-item has-dropdown is-hoverable sp-current">
|
||||
<a class="navbar-link" href="out-domains.html">Outbound</a>
|
||||
<div class="navbar-dropdown">
|
||||
<a class="navbar-item is-active" href="out-domains.html"><i class="ti ti-world-upload"></i>Domains</a>
|
||||
<a class="navbar-item" href="out-log.html"><i class="ti ti-list-details"></i>Log</a>
|
||||
<a class="navbar-item" href="out-queue.html"><i class="ti ti-stack-2"></i>Queue</a>
|
||||
<a class="navbar-item" href="dmarc.html"><i class="ti ti-report-analytics"></i>DMARC reports</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="navbar-item has-dropdown is-hoverable">
|
||||
<a class="navbar-link" href="in-domains.html">Inbound</a>
|
||||
<div class="navbar-dropdown">
|
||||
<a class="navbar-item" href="in-domains.html"><i class="ti ti-world-download"></i>Domains</a>
|
||||
<a class="navbar-item" href="in-log.html"><i class="ti ti-list-details"></i>Log</a>
|
||||
<a class="navbar-item" href="in-queue.html"><i class="ti ti-stack-2"></i>Queue</a>
|
||||
<a class="navbar-item" href="in-quarantine.html"><i class="ti ti-mail-pause"></i>Quarantine</a>
|
||||
<a class="navbar-item" href="in-filter-lists.html"><i class="ti ti-filter"></i>Filter lists</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="navbar-item has-dropdown is-hoverable">
|
||||
<a class="navbar-link" href="health.html">Server</a>
|
||||
<div class="navbar-dropdown">
|
||||
<a class="navbar-item" href="health.html"><i class="ti ti-heartbeat"></i>Health</a>
|
||||
<a class="navbar-item" href="preflight.html"><i class="ti ti-checklist"></i>Preflight</a>
|
||||
<a class="navbar-item" href="system-log.html"><i class="ti ti-file-text"></i>System log</a>
|
||||
<a class="navbar-item" href="backup.html"><i class="ti ti-archive"></i>Backup</a>
|
||||
<a class="navbar-item" href="users.html"><i class="ti ti-users"></i>Users</a>
|
||||
<a class="navbar-item" href="settings.html"><i class="ti ti-settings"></i>Settings</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="navbar-end">
|
||||
<div class="navbar-item has-dropdown is-hoverable">
|
||||
<a class="navbar-link" href="account.html"><i class="ti ti-user-circle"></i>admin</a>
|
||||
<div class="navbar-dropdown is-right">
|
||||
<a class="navbar-item" href="account.html"><i class="ti ti-key"></i>Account</a>
|
||||
<a class="navbar-item" href="help.html"><i class="ti ti-help"></i>Help</a>
|
||||
<hr class="navbar-divider">
|
||||
<form method="post" action="login.html"><button type="submit" class="navbar-item"><i class="ti ti-logout"></i>Sign out</button></form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
<div class="sp-perf" aria-hidden="true"></div>
|
||||
<div class="sp-subnav"><div class="container"><a href="out-domains.html" aria-current="page"><i class="ti ti-world-upload"></i>Domains</a><a href="out-log.html"><i class="ti ti-list-details"></i>Log</a><a href="out-queue.html"><i class="ti ti-stack-2"></i>Queue</a><a href="dmarc.html"><i class="ti ti-report-analytics"></i>DMARC reports</a></div></div>
|
||||
<main>
|
||||
<div class="container sp-page">
|
||||
<div class="sp-head">
|
||||
<div class="sp-grow">
|
||||
<p class="sp-kicker">Outbound</p>
|
||||
<h1 class="title is-3">Outbound domains</h1>
|
||||
<p class="sp-lead">Domains this relay signs and sends for. Each gets a DKIM key and its own application logins.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="box">
|
||||
<div class="sp-box-head"><span class="sp-no">01</span><h2>Add a domain</h2></div>
|
||||
<div class="sp-box-body">
|
||||
<form class="sp-form" method="post" action="out-domain.html">
|
||||
<div class="field has-addons">
|
||||
<div class="control is-expanded has-icons-left"><input class="input" name="name" type="text" placeholder="example.com" autocomplete="off" autocapitalize="none" spellcheck="false" required><span class="icon is-left"><i class="ti ti-world"></i></span></div>
|
||||
<div class="control"><button type="submit" class="button is-primary"><i class="ti ti-plus"></i>Add domain</button></div>
|
||||
</div>
|
||||
<p class="help">A DKIM key is generated at once; the next page shows the three DNS records to publish.
|
||||
Moving a domain from another SelfPost? <a href="backup.html#import">Import it</a> instead — the key and passwords come across.</p>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="box">
|
||||
<div class="sp-box-head"><span class="sp-no">02</span><h2>Domains</h2><div class="sp-end">3 domains · DNS cached a few minutes</div></div>
|
||||
<div class="table-container"><table class="table is-fullwidth is-hoverable">
|
||||
<thead><tr><th>Domain</th><th>DNS</th><th>Selector</th><th>Apps</th><th>30 days</th><th></th></tr></thead>
|
||||
<tbody>
|
||||
<tr><td><a href="out-domain.html"><strong>example.org</strong></a></td><td><span class="tag is-success is-light">DKIM</span> <span class="tag is-danger is-light">SPF</span> <span class="tag is-success is-light">DMARC</span></td><td class="sp-muted sp-mono">sp2026</td><td>2</td><td class="sp-muted">1 284 msg · peak 96/h</td><td class="sp-actions"><a href="out-domain.html">Open</a><a class="has-text-danger" href="out-domain-delete.html">Delete</a></td></tr>
|
||||
<tr><td><a href="out-domain.html"><strong>shop.example.org</strong></a></td><td><span class="tag is-success is-light">DKIM</span> <span class="tag is-success is-light">SPF</span> <span class="tag is-success is-light">DMARC</span></td><td class="sp-muted sp-mono">sp2026</td><td>1</td><td class="sp-muted">18 920 msg · peak 410/h</td><td class="sp-actions"><a href="out-domain.html">Open</a><a class="has-text-danger" href="out-domain-delete.html">Delete</a></td></tr>
|
||||
<tr><td><a href="out-domain.html"><strong>notify.acme.io</strong></a></td><td><span class="tag is-success is-light">DKIM</span> <span class="tag is-success is-light">SPF</span> <span class="tag is-warning is-light">DMARC</span></td><td class="sp-muted sp-mono">sp2025</td><td>3</td><td class="sp-muted">402 msg · peak 22/h</td><td class="sp-actions"><a href="out-domain.html">Open</a><a class="has-text-danger" href="out-domain-delete.html">Delete</a></td></tr>
|
||||
</tbody>
|
||||
</table></div>
|
||||
<div class="sp-box-foot">Each tag is one check. Green — published and correct; amber — published but weak; red — missing or wrong.</div>
|
||||
</div>
|
||||
|
||||
<footer class="sp-foot">SelfPost 1.8.0 · © 2026 Mix · <a href="#">License (AGPL-3.0)</a> · <a href="#">Source</a> · No warranty</footer>
|
||||
</div>
|
||||
</main>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,105 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en" data-theme="light">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>Outbound log · SelfPost</title>
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bulma@1.0.4/css/bulma.min.css">
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@tabler/icons-webfont@3/dist/tabler-icons.min.css">
|
||||
<link rel="stylesheet" href="../shared/brand.css">
|
||||
<link rel="stylesheet" href="theme.css">
|
||||
</head>
|
||||
<body>
|
||||
<nav class="navbar is-primary" aria-label="main navigation">
|
||||
<div class="container">
|
||||
<div class="navbar-brand"><a class="navbar-item" href="overview.html"><img src="../../selfpost-wordmark.svg" alt="SelfPost"></a></div>
|
||||
<div class="navbar-menu is-active">
|
||||
<div class="navbar-start">
|
||||
<a class="navbar-item" href="overview.html">Overview</a>
|
||||
<div class="navbar-item has-dropdown is-hoverable sp-current">
|
||||
<a class="navbar-link" href="out-domains.html">Outbound</a>
|
||||
<div class="navbar-dropdown">
|
||||
<a class="navbar-item" href="out-domains.html"><i class="ti ti-world-upload"></i>Domains</a>
|
||||
<a class="navbar-item is-active" href="out-log.html"><i class="ti ti-list-details"></i>Log</a>
|
||||
<a class="navbar-item" href="out-queue.html"><i class="ti ti-stack-2"></i>Queue</a>
|
||||
<a class="navbar-item" href="dmarc.html"><i class="ti ti-report-analytics"></i>DMARC reports</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="navbar-item has-dropdown is-hoverable">
|
||||
<a class="navbar-link" href="in-domains.html">Inbound</a>
|
||||
<div class="navbar-dropdown">
|
||||
<a class="navbar-item" href="in-domains.html"><i class="ti ti-world-download"></i>Domains</a>
|
||||
<a class="navbar-item" href="in-log.html"><i class="ti ti-list-details"></i>Log</a>
|
||||
<a class="navbar-item" href="in-queue.html"><i class="ti ti-stack-2"></i>Queue</a>
|
||||
<a class="navbar-item" href="in-quarantine.html"><i class="ti ti-mail-pause"></i>Quarantine</a>
|
||||
<a class="navbar-item" href="in-filter-lists.html"><i class="ti ti-filter"></i>Filter lists</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="navbar-item has-dropdown is-hoverable">
|
||||
<a class="navbar-link" href="health.html">Server</a>
|
||||
<div class="navbar-dropdown">
|
||||
<a class="navbar-item" href="health.html"><i class="ti ti-heartbeat"></i>Health</a>
|
||||
<a class="navbar-item" href="preflight.html"><i class="ti ti-checklist"></i>Preflight</a>
|
||||
<a class="navbar-item" href="system-log.html"><i class="ti ti-file-text"></i>System log</a>
|
||||
<a class="navbar-item" href="backup.html"><i class="ti ti-archive"></i>Backup</a>
|
||||
<a class="navbar-item" href="users.html"><i class="ti ti-users"></i>Users</a>
|
||||
<a class="navbar-item" href="settings.html"><i class="ti ti-settings"></i>Settings</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="navbar-end">
|
||||
<div class="navbar-item has-dropdown is-hoverable">
|
||||
<a class="navbar-link" href="account.html"><i class="ti ti-user-circle"></i>admin</a>
|
||||
<div class="navbar-dropdown is-right">
|
||||
<a class="navbar-item" href="account.html"><i class="ti ti-key"></i>Account</a>
|
||||
<a class="navbar-item" href="help.html"><i class="ti ti-help"></i>Help</a>
|
||||
<hr class="navbar-divider">
|
||||
<form method="post" action="login.html"><button type="submit" class="navbar-item"><i class="ti ti-logout"></i>Sign out</button></form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
<div class="sp-perf" aria-hidden="true"></div>
|
||||
<div class="sp-subnav"><div class="container"><a href="out-domains.html"><i class="ti ti-world-upload"></i>Domains</a><a href="out-log.html" aria-current="page"><i class="ti ti-list-details"></i>Log</a><a href="out-queue.html"><i class="ti ti-stack-2"></i>Queue</a><a href="dmarc.html"><i class="ti ti-report-analytics"></i>DMARC reports</a></div></div>
|
||||
<main>
|
||||
<div class="container sp-page">
|
||||
<div class="sp-head">
|
||||
<div class="sp-grow">
|
||||
<p class="sp-kicker">Outbound</p>
|
||||
<h1 class="title is-3">Outbound log</h1>
|
||||
<p class="sp-lead">Every message an application handed to the relay. Rows older than 30 days are deleted — change that in <a href="settings.html">Settings</a>.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="box">
|
||||
<div class="sp-box-head">
|
||||
<span class="sp-no">01</span><h2>Messages</h2>
|
||||
<form class="sp-end" method="get" action="#">
|
||||
<div class="select"><select name="domain"><option>All domains</option><option>example.org</option><option>shop.example.org</option><option>notify.acme.io</option></select></div>
|
||||
<div class="select"><select name="app"><option>All applications</option><option>prod-server</option><option>alerts</option></select></div>
|
||||
<div class="select"><select name="status"><option>Any status</option><option>sent</option><option>deferred</option><option>bounced</option><option>rate-limited</option></select></div>
|
||||
<button type="submit" class="button"><i class="ti ti-filter"></i>Filter</button>
|
||||
</form>
|
||||
<a class="sp-help" href="help.html#log" title="What the statuses mean"><i class="ti ti-help-circle"></i></a>
|
||||
</div>
|
||||
<div class="table-container"><table class="table is-fullwidth is-hoverable">
|
||||
<thead><tr><th>Time, UTC</th><th>From</th><th>To</th><th>Subject</th><th>Status</th><th></th></tr></thead>
|
||||
<tbody>
|
||||
<tr><td class="sp-mono sp-muted sp-nowrap sp-small">09-21 14:01:52</td><td>shop@<wbr>shop.example.org</td><td>m.keller@<wbr>gmx.de</td><td class="sp-clip">Your order #48213 has shipped</td><td><span class="tag is-success is-light">sent</span></td><td class="sp-actions"><a href="out-message.html">Details</a></td></tr>
|
||||
<tr><td class="sp-mono sp-muted sp-nowrap sp-small">09-21 14:01:50</td><td>shop@<wbr>shop.example.org</td><td>anna.lind@<wbr>outlook.com</td><td class="sp-clip">Your order #48212 has shipped</td><td><span class="tag is-success is-light">sent</span></td><td class="sp-actions"><a href="out-message.html">Details</a></td></tr>
|
||||
<tr><td class="sp-mono sp-muted sp-nowrap sp-small">09-21 13:58:07</td><td>alerts@<wbr>example.org</td><td>noc@<wbr>example.org</td><td class="sp-clip">[FIRING] disk usage above 90 % on db-2</td><td><span class="tag is-warning is-light">deferred</span></td><td class="sp-actions"><a href="out-message.html">Details</a></td></tr>
|
||||
<tr><td class="sp-mono sp-muted sp-nowrap sp-small">09-21 13:41:19</td><td>no-reply@<wbr>notify.acme.io</td><td>j.doe@<wbr>nonexistent.example</td><td class="sp-clip">Reset your password</td><td><span class="tag is-danger is-light">bounced</span></td><td class="sp-actions"><a href="out-message.html">Details</a></td></tr>
|
||||
<tr><td class="sp-mono sp-muted sp-nowrap sp-small">09-21 13:40:02</td><td>prod@<wbr>example.org</td><td>billing@<wbr>partner.example</td><td class="sp-clip">Invoice 2026-0917</td><td><span class="tag is-light">rate-limited</span></td><td class="sp-actions"><a href="out-message.html">Details</a></td></tr>
|
||||
<tr><td class="sp-mono sp-muted sp-nowrap sp-small">09-21 13:12:44</td><td>shop@<wbr>shop.example.org</td><td>p.novak@<wbr>seznam.cz</td><td class="sp-clip">We received your return request</td><td><span class="tag is-success is-light">sent</span></td><td class="sp-actions"><a href="out-message.html">Details</a></td></tr>
|
||||
</tbody>
|
||||
</table></div>
|
||||
<div class="sp-box-foot"><span>Page 1 of 412</span><a href="#">Older →</a><span class="sp-end">New rows appear on their own</span></div>
|
||||
</div>
|
||||
|
||||
<footer class="sp-foot">SelfPost 1.8.0 · © 2026 Mix · <a href="#">License (AGPL-3.0)</a> · <a href="#">Source</a> · No warranty</footer>
|
||||
</div>
|
||||
</main>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,118 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en" data-theme="light">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>Message · Outbound log · SelfPost</title>
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bulma@1.0.4/css/bulma.min.css">
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@tabler/icons-webfont@3/dist/tabler-icons.min.css">
|
||||
<link rel="stylesheet" href="../shared/brand.css">
|
||||
<link rel="stylesheet" href="theme.css">
|
||||
</head>
|
||||
<body>
|
||||
<nav class="navbar is-primary" aria-label="main navigation">
|
||||
<div class="container">
|
||||
<div class="navbar-brand"><a class="navbar-item" href="overview.html"><img src="../../selfpost-wordmark.svg" alt="SelfPost"></a></div>
|
||||
<div class="navbar-menu is-active">
|
||||
<div class="navbar-start">
|
||||
<a class="navbar-item" href="overview.html">Overview</a>
|
||||
<div class="navbar-item has-dropdown is-hoverable sp-current">
|
||||
<a class="navbar-link" href="out-domains.html">Outbound</a>
|
||||
<div class="navbar-dropdown">
|
||||
<a class="navbar-item" href="out-domains.html"><i class="ti ti-world-upload"></i>Domains</a>
|
||||
<a class="navbar-item is-active" href="out-log.html"><i class="ti ti-list-details"></i>Log</a>
|
||||
<a class="navbar-item" href="out-queue.html"><i class="ti ti-stack-2"></i>Queue</a>
|
||||
<a class="navbar-item" href="dmarc.html"><i class="ti ti-report-analytics"></i>DMARC reports</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="navbar-item has-dropdown is-hoverable">
|
||||
<a class="navbar-link" href="in-domains.html">Inbound</a>
|
||||
<div class="navbar-dropdown">
|
||||
<a class="navbar-item" href="in-domains.html"><i class="ti ti-world-download"></i>Domains</a>
|
||||
<a class="navbar-item" href="in-log.html"><i class="ti ti-list-details"></i>Log</a>
|
||||
<a class="navbar-item" href="in-queue.html"><i class="ti ti-stack-2"></i>Queue</a>
|
||||
<a class="navbar-item" href="in-quarantine.html"><i class="ti ti-mail-pause"></i>Quarantine</a>
|
||||
<a class="navbar-item" href="in-filter-lists.html"><i class="ti ti-filter"></i>Filter lists</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="navbar-item has-dropdown is-hoverable">
|
||||
<a class="navbar-link" href="health.html">Server</a>
|
||||
<div class="navbar-dropdown">
|
||||
<a class="navbar-item" href="health.html"><i class="ti ti-heartbeat"></i>Health</a>
|
||||
<a class="navbar-item" href="preflight.html"><i class="ti ti-checklist"></i>Preflight</a>
|
||||
<a class="navbar-item" href="system-log.html"><i class="ti ti-file-text"></i>System log</a>
|
||||
<a class="navbar-item" href="backup.html"><i class="ti ti-archive"></i>Backup</a>
|
||||
<a class="navbar-item" href="users.html"><i class="ti ti-users"></i>Users</a>
|
||||
<a class="navbar-item" href="settings.html"><i class="ti ti-settings"></i>Settings</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="navbar-end">
|
||||
<div class="navbar-item has-dropdown is-hoverable">
|
||||
<a class="navbar-link" href="account.html"><i class="ti ti-user-circle"></i>admin</a>
|
||||
<div class="navbar-dropdown is-right">
|
||||
<a class="navbar-item" href="account.html"><i class="ti ti-key"></i>Account</a>
|
||||
<a class="navbar-item" href="help.html"><i class="ti ti-help"></i>Help</a>
|
||||
<hr class="navbar-divider">
|
||||
<form method="post" action="login.html"><button type="submit" class="navbar-item"><i class="ti ti-logout"></i>Sign out</button></form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
<div class="sp-perf" aria-hidden="true"></div>
|
||||
<div class="sp-subnav"><div class="container"><a href="out-domains.html"><i class="ti ti-world-upload"></i>Domains</a><a href="out-log.html" aria-current="page"><i class="ti ti-list-details"></i>Log</a><a href="out-queue.html"><i class="ti ti-stack-2"></i>Queue</a><a href="dmarc.html"><i class="ti ti-report-analytics"></i>DMARC reports</a></div></div>
|
||||
<main>
|
||||
<div class="container sp-page">
|
||||
<div class="sp-head">
|
||||
<div class="sp-grow">
|
||||
<p class="sp-kicker"><a href="out-log.html">Outbound log</a> /</p>
|
||||
<h1 class="title is-4">[FIRING] disk usage above 90 % on db-2</h1>
|
||||
<p class="sp-route"><span>alerts@example.org</span><i class="ti ti-arrow-right sp-muted"></i><span>noc@example.org</span><span class="tag is-warning is-light">deferred</span></p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="columns">
|
||||
<div class="column is-7">
|
||||
<div class="box">
|
||||
<div class="sp-box-head"><span class="sp-no">01</span><h2>Message</h2></div>
|
||||
<div class="sp-box-body">
|
||||
<dl class="sp-facts">
|
||||
<div><dt>Domain</dt><dd><a href="out-domain.html">example.org</a></dd></div>
|
||||
<div><dt>Application</dt><dd class="sp-mono"><a href="out-app.html">alerts</a></dd></div>
|
||||
<div><dt>Accepted</dt><dd>2026-09-21 13:58:07 UTC</dd></div>
|
||||
<div><dt>Status reported</dt><dd>2026-09-21 13:58:09 UTC</dd></div>
|
||||
<div><dt>Queue id</dt><dd class="sp-mono">4XcB7k2Jm9z1</dd></div>
|
||||
<div><dt>Journal id</dt><dd class="sp-mono">184220</dd></div>
|
||||
</dl>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="column is-5">
|
||||
<div class="box">
|
||||
<div class="sp-box-head"><span class="sp-no">02</span><h2>History</h2><a class="sp-help" href="help.html#log" title="What the statuses mean"><i class="ti ti-help-circle"></i></a></div>
|
||||
<div class="sp-box-body">
|
||||
<ol class="sp-timeline">
|
||||
<li><time>13:58:07 UTC</time><strong>Accepted</strong> from <span class="sp-mono">alerts</span> at 198.51.100.7</li>
|
||||
<li class="sp-warn"><time>13:58:09 UTC</time><strong>Deferred</strong> — the receiving server asked to retry later</li>
|
||||
<li class="sp-pending"><time>next attempt ≈ 14:08 UTC</time>Still in the <a href="out-queue.html">queue</a>; kept for 5 days, then bounced</li>
|
||||
</ol>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="box">
|
||||
<div class="sp-box-head"><span class="sp-no">03</span><h2>Delivery log</h2><div class="sp-end">Lines from <code>mail.log</code> with this queue id</div></div>
|
||||
<pre class="sp-log"><span class="sp-d">13:58:07</span> postfix/smtpd[2114]: 4XcB7k2Jm9z1: client=unknown[198.51.100.7], sasl_username=alerts
|
||||
<span class="sp-d">13:58:07</span> postfix/cleanup[2120]: 4XcB7k2Jm9z1: message-id=<a81f0c@alertmanager>
|
||||
<span class="sp-d">13:58:07</span> opendkim[39]: 4XcB7k2Jm9z1: DKIM-Signature field added (s=sp2026, d=example.org)
|
||||
<span class="sp-d">13:58:09</span> <span class="sp-w">postfix/smtp[2123]: 4XcB7k2Jm9z1: to=<noc@example.org>, relay=mx1.example.org[192.0.2.10]:25, delay=1.9, status=deferred (host mx1.example.org said: 451 4.7.1 Greylisted, try again in 5 minutes)</span></pre>
|
||||
</div>
|
||||
|
||||
<footer class="sp-foot">SelfPost 1.8.0 · © 2026 Mix · <a href="#">License (AGPL-3.0)</a> · <a href="#">Source</a> · No warranty</footer>
|
||||
</div>
|
||||
</main>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,102 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en" data-theme="light">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>Outbound queue · SelfPost</title>
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bulma@1.0.4/css/bulma.min.css">
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@tabler/icons-webfont@3/dist/tabler-icons.min.css">
|
||||
<link rel="stylesheet" href="../shared/brand.css">
|
||||
<link rel="stylesheet" href="theme.css">
|
||||
</head>
|
||||
<body>
|
||||
<nav class="navbar is-primary" aria-label="main navigation">
|
||||
<div class="container">
|
||||
<div class="navbar-brand"><a class="navbar-item" href="overview.html"><img src="../../selfpost-wordmark.svg" alt="SelfPost"></a></div>
|
||||
<div class="navbar-menu is-active">
|
||||
<div class="navbar-start">
|
||||
<a class="navbar-item" href="overview.html">Overview</a>
|
||||
<div class="navbar-item has-dropdown is-hoverable sp-current">
|
||||
<a class="navbar-link" href="out-domains.html">Outbound</a>
|
||||
<div class="navbar-dropdown">
|
||||
<a class="navbar-item" href="out-domains.html"><i class="ti ti-world-upload"></i>Domains</a>
|
||||
<a class="navbar-item" href="out-log.html"><i class="ti ti-list-details"></i>Log</a>
|
||||
<a class="navbar-item is-active" href="out-queue.html"><i class="ti ti-stack-2"></i>Queue</a>
|
||||
<a class="navbar-item" href="dmarc.html"><i class="ti ti-report-analytics"></i>DMARC reports</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="navbar-item has-dropdown is-hoverable">
|
||||
<a class="navbar-link" href="in-domains.html">Inbound</a>
|
||||
<div class="navbar-dropdown">
|
||||
<a class="navbar-item" href="in-domains.html"><i class="ti ti-world-download"></i>Domains</a>
|
||||
<a class="navbar-item" href="in-log.html"><i class="ti ti-list-details"></i>Log</a>
|
||||
<a class="navbar-item" href="in-queue.html"><i class="ti ti-stack-2"></i>Queue</a>
|
||||
<a class="navbar-item" href="in-quarantine.html"><i class="ti ti-mail-pause"></i>Quarantine</a>
|
||||
<a class="navbar-item" href="in-filter-lists.html"><i class="ti ti-filter"></i>Filter lists</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="navbar-item has-dropdown is-hoverable">
|
||||
<a class="navbar-link" href="health.html">Server</a>
|
||||
<div class="navbar-dropdown">
|
||||
<a class="navbar-item" href="health.html"><i class="ti ti-heartbeat"></i>Health</a>
|
||||
<a class="navbar-item" href="preflight.html"><i class="ti ti-checklist"></i>Preflight</a>
|
||||
<a class="navbar-item" href="system-log.html"><i class="ti ti-file-text"></i>System log</a>
|
||||
<a class="navbar-item" href="backup.html"><i class="ti ti-archive"></i>Backup</a>
|
||||
<a class="navbar-item" href="users.html"><i class="ti ti-users"></i>Users</a>
|
||||
<a class="navbar-item" href="settings.html"><i class="ti ti-settings"></i>Settings</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="navbar-end">
|
||||
<div class="navbar-item has-dropdown is-hoverable">
|
||||
<a class="navbar-link" href="account.html"><i class="ti ti-user-circle"></i>admin</a>
|
||||
<div class="navbar-dropdown is-right">
|
||||
<a class="navbar-item" href="account.html"><i class="ti ti-key"></i>Account</a>
|
||||
<a class="navbar-item" href="help.html"><i class="ti ti-help"></i>Help</a>
|
||||
<hr class="navbar-divider">
|
||||
<form method="post" action="login.html"><button type="submit" class="navbar-item"><i class="ti ti-logout"></i>Sign out</button></form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
<div class="sp-perf" aria-hidden="true"></div>
|
||||
<div class="sp-subnav"><div class="container"><a href="out-domains.html"><i class="ti ti-world-upload"></i>Domains</a><a href="out-log.html"><i class="ti ti-list-details"></i>Log</a><a href="out-queue.html" aria-current="page"><i class="ti ti-stack-2"></i>Queue</a><a href="dmarc.html"><i class="ti ti-report-analytics"></i>DMARC reports</a></div></div>
|
||||
<main>
|
||||
<div class="container sp-page">
|
||||
<div class="sp-head">
|
||||
<div class="sp-grow">
|
||||
<p class="sp-kicker">Outbound</p>
|
||||
<h1 class="title is-3">Outbound queue</h1>
|
||||
<p class="sp-lead">Messages accepted from applications and still waiting for the receiving server. Refreshes every 10 s.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="box">
|
||||
<div class="sp-box-head"><span class="sp-no">01</span><h2>Waiting</h2><div class="sp-end">1 message · 3.1 kB</div><a class="sp-help" href="help.html#queue" title="What waits here and how Postfix retries"><i class="ti ti-help-circle"></i></a></div>
|
||||
<div class="table-container"><table class="table is-fullwidth is-hoverable">
|
||||
<thead><tr><th>Queue id</th><th>Arrived, UTC</th><th>From</th><th>To</th><th>Last reply</th><th></th></tr></thead>
|
||||
<tbody>
|
||||
<tr><td class="sp-mono">4XcB7k2Jm9z1</td><td class="sp-mono sp-muted sp-nowrap sp-small">09-21 13:58:07</td><td>alerts@<wbr>example.org</td><td>noc@<wbr>example.org</td><td class="sp-muted">451 4.7.1 Greylisted, try again in 5 minutes</td><td class="sp-actions"><a href="out-message.html">Details</a></td></tr>
|
||||
</tbody>
|
||||
</table></div>
|
||||
</div>
|
||||
|
||||
<div class="box">
|
||||
<div class="sp-box-head"><span class="sp-no">02</span><h2>How retries work</h2><div class="sp-end">This Postfix's policy, read at panel start</div></div>
|
||||
<div class="sp-box-body">
|
||||
<dl class="sp-facts">
|
||||
<div><dt>First retry</dt><dd class="sp-big">5 min</dd></div>
|
||||
<div><dt>Later retries</dt><dd class="sp-big">doubling</dd><dd class="sp-muted">capped at 66 min</dd></div>
|
||||
<div><dt>Kept in queue</dt><dd class="sp-big">5 days</dd></div>
|
||||
<div><dt>Then</dt><dd class="sp-big">bounced</dd><dd class="sp-muted">no attempt counter, only time</dd></div>
|
||||
</dl>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<footer class="sp-foot">SelfPost 1.8.0 · © 2026 Mix · <a href="#">License (AGPL-3.0)</a> · <a href="#">Source</a> · No warranty</footer>
|
||||
</div>
|
||||
</main>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,466 @@
|
||||
{
|
||||
"account": [
|
||||
"head",
|
||||
{
|
||||
"columns": [
|
||||
{
|
||||
"is-6": [
|
||||
"box[Profile]:body"
|
||||
]
|
||||
},
|
||||
{
|
||||
"is-6": [
|
||||
"box[Password]:body"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"backup": [
|
||||
"head",
|
||||
{
|
||||
"columns": [
|
||||
{
|
||||
"is-6": [
|
||||
"box[Full backup]:body"
|
||||
]
|
||||
},
|
||||
{
|
||||
"is-6": [
|
||||
"box[Import a domain]:body"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"dmarc-domain": [
|
||||
"head+postmark+actions",
|
||||
{
|
||||
"columns": [
|
||||
{
|
||||
"is-5": [
|
||||
"box[Sources · 7 days]:table"
|
||||
]
|
||||
},
|
||||
{
|
||||
"is-7": [
|
||||
"box[Reports]:table"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"dmarc-report": [
|
||||
"head",
|
||||
{
|
||||
"columns": [
|
||||
{
|
||||
"is-6": [
|
||||
"box[Report]:body"
|
||||
]
|
||||
},
|
||||
{
|
||||
"is-6": [
|
||||
"box[Published policy]:body"
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
"box[Records]:table"
|
||||
],
|
||||
"dmarc": [
|
||||
"head",
|
||||
"box[Ingest]:body",
|
||||
"box[Recent reports]:table"
|
||||
],
|
||||
"health": [
|
||||
"head+actions",
|
||||
{
|
||||
"columns": [
|
||||
{
|
||||
"is-6": [
|
||||
"box[Machine]:table"
|
||||
]
|
||||
},
|
||||
{
|
||||
"is-6": [
|
||||
"box[Processes]:table"
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"columns": [
|
||||
{
|
||||
"is-4": [
|
||||
"box[TLS certificate]:body"
|
||||
]
|
||||
},
|
||||
{
|
||||
"is-4": [
|
||||
"box[Milter sockets]:table"
|
||||
]
|
||||
},
|
||||
{
|
||||
"is-4": [
|
||||
"box[Hostname and reverse DNS]:body"
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
"box[Configuration]:body"
|
||||
],
|
||||
"help": [
|
||||
"head",
|
||||
{
|
||||
"columns": [
|
||||
{
|
||||
"is-3": [
|
||||
"side_menu"
|
||||
]
|
||||
},
|
||||
{
|
||||
"is-9": [
|
||||
"box[Overview]:help×3",
|
||||
"box[Outbound]:help×6",
|
||||
"box[Inbound]:help×4",
|
||||
"box[Server]:help×3",
|
||||
"box[Account]:help"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"in-domain-delete": [
|
||||
"head",
|
||||
{
|
||||
"columns": [
|
||||
{
|
||||
"is-7": [
|
||||
"box.sp-danger-zone[This cannot be undone]:body"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"in-domain": [
|
||||
"head+postmark+actions",
|
||||
{
|
||||
"columns": [
|
||||
{
|
||||
"is-3": [
|
||||
"side_menu"
|
||||
]
|
||||
},
|
||||
{
|
||||
"is-9": [
|
||||
"box[MX record]:record",
|
||||
{
|
||||
"columns": [
|
||||
{
|
||||
"is-6": [
|
||||
"box[Upstream]:body"
|
||||
]
|
||||
},
|
||||
{
|
||||
"is-6": [
|
||||
"box[Valid recipients]:body"
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
"box[Spam filter]:body"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"in-domains": [
|
||||
"head",
|
||||
"box[Add a domain]:body",
|
||||
"box[Domains]:table"
|
||||
],
|
||||
"in-filter-lists": [
|
||||
"head",
|
||||
"box[Add an entry]:body",
|
||||
{
|
||||
"columns": [
|
||||
{
|
||||
"is-6": [
|
||||
"box[Deny]:table"
|
||||
]
|
||||
},
|
||||
{
|
||||
"is-6": [
|
||||
"box[Allow]:table"
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
"box[Where the lists go]:body"
|
||||
],
|
||||
"in-log": [
|
||||
"head",
|
||||
"box[Messages]:table"
|
||||
],
|
||||
"in-quarantine-message": [
|
||||
"head",
|
||||
{
|
||||
"columns": [
|
||||
{
|
||||
"is-7": [
|
||||
"box[Message]:body"
|
||||
]
|
||||
},
|
||||
{
|
||||
"is-5": [
|
||||
"box[Why it was held]:table"
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
"box[Headers]:log",
|
||||
{
|
||||
"columns": [
|
||||
{
|
||||
"is-6": [
|
||||
"box[Release]:body"
|
||||
]
|
||||
},
|
||||
{
|
||||
"is-6": [
|
||||
"box.sp-danger-zone[Discard]:body"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"in-quarantine": [
|
||||
"head",
|
||||
"box[Held messages]:table"
|
||||
],
|
||||
"in-queue": [
|
||||
"head",
|
||||
"box[Waiting]:empty",
|
||||
"box[How retries work]:body"
|
||||
],
|
||||
"out-app-created": [
|
||||
"head",
|
||||
{
|
||||
"columns": [
|
||||
{
|
||||
"is-7": [
|
||||
"box.sp-credential[Shown once]:record"
|
||||
]
|
||||
},
|
||||
{
|
||||
"is-5": [
|
||||
"box[Connect with]:body"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"out-app": [
|
||||
"head+actions",
|
||||
{
|
||||
"form": [
|
||||
{
|
||||
"columns": [
|
||||
{
|
||||
"is-4": [
|
||||
"box[Sender]:body"
|
||||
]
|
||||
},
|
||||
{
|
||||
"is-4": [
|
||||
"box[Client IP allow-list]:body"
|
||||
]
|
||||
},
|
||||
{
|
||||
"is-4": [
|
||||
"box[Rate limit]:body"
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
"submit_row"
|
||||
]
|
||||
}
|
||||
],
|
||||
"out-domain-delete": [
|
||||
"head",
|
||||
{
|
||||
"columns": [
|
||||
{
|
||||
"is-7": [
|
||||
"box.sp-danger-zone[This cannot be undone]:body"
|
||||
]
|
||||
},
|
||||
{
|
||||
"is-5": [
|
||||
"box[Moving it instead?]:body"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"out-domain-settings": [
|
||||
"head",
|
||||
{
|
||||
"columns": [
|
||||
{
|
||||
"is-6": [
|
||||
"box[DMARC report address]:body",
|
||||
"box[Export domain]:body"
|
||||
]
|
||||
},
|
||||
{
|
||||
"is-6": [
|
||||
"box[Rate limit]:body",
|
||||
"box.sp-danger-zone[Delete domain]:body"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"out-domain": [
|
||||
"head+postmark+actions",
|
||||
{
|
||||
"columns": [
|
||||
{
|
||||
"is-3": [
|
||||
"side_menu"
|
||||
]
|
||||
},
|
||||
{
|
||||
"is-9": [
|
||||
"box[DNS records]:record×3",
|
||||
"box[Applications]:table",
|
||||
"box[Connection]:body"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"out-domains": [
|
||||
"head",
|
||||
"box[Add a domain]:body",
|
||||
"box[Domains]:table"
|
||||
],
|
||||
"out-log": [
|
||||
"head",
|
||||
"box[Messages]:table"
|
||||
],
|
||||
"out-message": [
|
||||
"head",
|
||||
{
|
||||
"columns": [
|
||||
{
|
||||
"is-7": [
|
||||
"box[Message]:body"
|
||||
]
|
||||
},
|
||||
{
|
||||
"is-5": [
|
||||
"box[History]:body"
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
"box[Delivery log]:log"
|
||||
],
|
||||
"out-queue": [
|
||||
"head",
|
||||
"box[Waiting]:table",
|
||||
"box[How retries work]:body"
|
||||
],
|
||||
"overview": [
|
||||
"head+postmark",
|
||||
"box[Server health]:health",
|
||||
{
|
||||
"columns": [
|
||||
{
|
||||
"is-7": [
|
||||
"box[Outbound domains]:table"
|
||||
]
|
||||
},
|
||||
{
|
||||
"is-5": [
|
||||
"box[Inbound domains]:table"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"preflight": [
|
||||
"head+postmark+actions",
|
||||
"box[Checks]:table",
|
||||
"box[Send a test e-mail]:body"
|
||||
],
|
||||
"settings": [
|
||||
"head",
|
||||
{
|
||||
"form": [
|
||||
{
|
||||
"columns": [
|
||||
{
|
||||
"is-6": [
|
||||
"box[Log retention]:body"
|
||||
]
|
||||
},
|
||||
{
|
||||
"is-6": [
|
||||
"box[Sending rate limits]:body"
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
"submit_row"
|
||||
]
|
||||
}
|
||||
],
|
||||
"system-log": [
|
||||
"head",
|
||||
"box[Recent lines]:log"
|
||||
],
|
||||
"user-delete": [
|
||||
"head",
|
||||
{
|
||||
"columns": [
|
||||
{
|
||||
"is-7": [
|
||||
"box.sp-danger-zone[This cannot be undone]:body"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"user": [
|
||||
"head+actions",
|
||||
{
|
||||
"form": [
|
||||
{
|
||||
"columns": [
|
||||
{
|
||||
"is-6": [
|
||||
"box[Account]:body"
|
||||
]
|
||||
},
|
||||
{
|
||||
"is-6": [
|
||||
"box[Outbound domains]:body",
|
||||
"box[Inbound domains]:body"
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
"submit_row"
|
||||
]
|
||||
}
|
||||
],
|
||||
"users": [
|
||||
"head+actions",
|
||||
"box[Panel users]:table"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,122 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en" data-theme="light">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>Overview · SelfPost</title>
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bulma@1.0.4/css/bulma.min.css">
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@tabler/icons-webfont@3/dist/tabler-icons.min.css">
|
||||
<link rel="stylesheet" href="../shared/brand.css">
|
||||
<link rel="stylesheet" href="theme.css">
|
||||
</head>
|
||||
<body>
|
||||
<nav class="navbar is-primary" aria-label="main navigation">
|
||||
<div class="container">
|
||||
<div class="navbar-brand"><a class="navbar-item" href="overview.html"><img src="../../selfpost-wordmark.svg" alt="SelfPost"></a></div>
|
||||
<div class="navbar-menu is-active">
|
||||
<div class="navbar-start">
|
||||
<a class="navbar-item sp-current" href="overview.html">Overview</a>
|
||||
<div class="navbar-item has-dropdown is-hoverable">
|
||||
<a class="navbar-link" href="out-domains.html">Outbound</a>
|
||||
<div class="navbar-dropdown">
|
||||
<a class="navbar-item" href="out-domains.html"><i class="ti ti-world-upload"></i>Domains</a>
|
||||
<a class="navbar-item" href="out-log.html"><i class="ti ti-list-details"></i>Log</a>
|
||||
<a class="navbar-item" href="out-queue.html"><i class="ti ti-stack-2"></i>Queue</a>
|
||||
<a class="navbar-item" href="dmarc.html"><i class="ti ti-report-analytics"></i>DMARC reports</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="navbar-item has-dropdown is-hoverable">
|
||||
<a class="navbar-link" href="in-domains.html">Inbound</a>
|
||||
<div class="navbar-dropdown">
|
||||
<a class="navbar-item" href="in-domains.html"><i class="ti ti-world-download"></i>Domains</a>
|
||||
<a class="navbar-item" href="in-log.html"><i class="ti ti-list-details"></i>Log</a>
|
||||
<a class="navbar-item" href="in-queue.html"><i class="ti ti-stack-2"></i>Queue</a>
|
||||
<a class="navbar-item" href="in-quarantine.html"><i class="ti ti-mail-pause"></i>Quarantine</a>
|
||||
<a class="navbar-item" href="in-filter-lists.html"><i class="ti ti-filter"></i>Filter lists</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="navbar-item has-dropdown is-hoverable">
|
||||
<a class="navbar-link" href="health.html">Server</a>
|
||||
<div class="navbar-dropdown">
|
||||
<a class="navbar-item" href="health.html"><i class="ti ti-heartbeat"></i>Health</a>
|
||||
<a class="navbar-item" href="preflight.html"><i class="ti ti-checklist"></i>Preflight</a>
|
||||
<a class="navbar-item" href="system-log.html"><i class="ti ti-file-text"></i>System log</a>
|
||||
<a class="navbar-item" href="backup.html"><i class="ti ti-archive"></i>Backup</a>
|
||||
<a class="navbar-item" href="users.html"><i class="ti ti-users"></i>Users</a>
|
||||
<a class="navbar-item" href="settings.html"><i class="ti ti-settings"></i>Settings</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="navbar-end">
|
||||
<div class="navbar-item has-dropdown is-hoverable">
|
||||
<a class="navbar-link" href="account.html"><i class="ti ti-user-circle"></i>admin</a>
|
||||
<div class="navbar-dropdown is-right">
|
||||
<a class="navbar-item" href="account.html"><i class="ti ti-key"></i>Account</a>
|
||||
<a class="navbar-item" href="help.html"><i class="ti ti-help"></i>Help</a>
|
||||
<hr class="navbar-divider">
|
||||
<form method="post" action="login.html"><button type="submit" class="navbar-item"><i class="ti ti-logout"></i>Sign out</button></form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
<div class="sp-perf" aria-hidden="true"></div>
|
||||
|
||||
<main>
|
||||
<div class="container sp-page">
|
||||
<div class="sp-head">
|
||||
<div class="sp-postmark sp-warn"><span>SELFPOST</span><b>WARN</b><span>1 OF 6 CHECKS</span></div>
|
||||
<div class="sp-grow">
|
||||
<p class="sp-kicker">mail.example.org</p>
|
||||
<h1 class="title is-3">Mail is flowing. One thing needs attention.</h1>
|
||||
<p class="sp-lead">The TLS certificate expires in 12 days and has not renewed yet.</p>
|
||||
</div>
|
||||
<div class="sp-muted sp-small">Updated 14:02:31 UTC<br>refreshes every 10 s</div>
|
||||
</div>
|
||||
|
||||
<div class="box">
|
||||
<div class="sp-box-head"><span class="sp-no">01</span><h2>Server health</h2><div class="sp-end"><a href="health.html">All details</a></div><a class="sp-help" href="help.html#checks" title="What these checks mean"><i class="ti ti-help-circle"></i></a></div>
|
||||
<div class="sp-health">
|
||||
<a href="health.html"><span class="sp-h-icon"><i class="ti ti-cpu"></i></span><span><span class="sp-h-name">Machine</span><span class="sp-h-value">CPU 12 %</span><span class="sp-h-sub">RAM 41 % · 62 kB/s</span></span></a>
|
||||
<a href="health.html"><span class="sp-h-icon"><i class="ti ti-server-cog"></i></span><span><span class="sp-h-name">Processes</span><span class="sp-h-value">5 of 5 running</span><span class="sp-h-sub">up 18 days</span></span></a>
|
||||
<a href="health.html" class="sp-warn"><span class="sp-h-icon"><i class="ti ti-certificate"></i></span><span><span class="sp-h-name">TLS certificate</span><span class="sp-h-value">Expires in 12 days</span><span class="sp-h-sub">renewal has not run</span></span></a>
|
||||
<a href="out-queue.html"><span class="sp-h-icon"><i class="ti ti-stack-2"></i></span><span><span class="sp-h-name">Queues</span><span class="sp-h-value">Both empty</span><span class="sp-h-sub">outbound and inbound</span></span></a>
|
||||
<a href="health.html"><span class="sp-h-icon"><i class="ti ti-plug-connected"></i></span><span><span class="sp-h-name">Milter sockets</span><span class="sp-h-value">3 of 3 answering</span><span class="sp-h-sub">incl. the spam filter</span></span></a>
|
||||
<a href="health.html"><span class="sp-h-icon"><i class="ti ti-arrows-exchange"></i></span><span><span class="sp-h-name">Reverse DNS</span><span class="sp-h-value">Forward = reverse</span><span class="sp-h-sub">203.0.113.25</span></span></a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="columns">
|
||||
<div class="column is-7">
|
||||
<div class="box">
|
||||
<div class="sp-box-head"><span class="sp-no">02</span><h2>Outbound domains</h2><div class="sp-end"><a href="out-domains.html">Manage</a></div></div>
|
||||
<div class="table-container"><table class="table is-fullwidth is-hoverable">
|
||||
<thead><tr><th>Domain</th><th>DNS</th><th>Apps</th><th>30 days</th></tr></thead>
|
||||
<tbody>
|
||||
<tr><td><a href="out-domain.html"><strong>example.org</strong></a></td><td><span class="tag is-success is-light">DKIM</span> <span class="tag is-danger is-light">SPF</span> <span class="tag is-success is-light">DMARC</span></td><td>2</td><td class="sp-muted">1 284 msg</td></tr>
|
||||
<tr><td><a href="out-domain.html"><strong>shop.example.org</strong></a></td><td><span class="tag is-success is-light">DKIM</span> <span class="tag is-success is-light">SPF</span> <span class="tag is-success is-light">DMARC</span></td><td>1</td><td class="sp-muted">18 920 msg</td></tr>
|
||||
<tr><td><a href="out-domain.html"><strong>notify.acme.io</strong></a></td><td><span class="tag is-success is-light">DKIM</span> <span class="tag is-success is-light">SPF</span> <span class="tag is-warning is-light">DMARC</span></td><td>3</td><td class="sp-muted">402 msg</td></tr>
|
||||
</tbody>
|
||||
</table></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="column is-5">
|
||||
<div class="box">
|
||||
<div class="sp-box-head"><span class="sp-no">03</span><h2>Inbound domains</h2><div class="sp-end"><a href="in-domains.html">Manage</a></div></div>
|
||||
<div class="table-container"><table class="table is-fullwidth is-hoverable">
|
||||
<thead><tr><th>Domain</th><th>MX</th><th>Upstream</th></tr></thead>
|
||||
<tbody>
|
||||
<tr><td><a href="in-domain.html"><strong>lists.example.org</strong></a></td><td><span class="tag is-success is-light">MX</span></td><td class="sp-muted sp-mono sp-small">mx.internal:25</td></tr>
|
||||
<tr><td><a href="in-domain.html"><strong>acme.io</strong></a></td><td><span class="tag is-success is-light">MX</span></td><td class="sp-muted sp-mono sp-small">10.0.4.12:2525</td></tr>
|
||||
</tbody>
|
||||
</table></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<footer class="sp-foot">SelfPost 1.8.0 · © 2026 Mix · <a href="#">License (AGPL-3.0)</a> · <a href="#">Source</a> · No warranty</footer>
|
||||
</div>
|
||||
</main>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,6 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="8" height="4" viewBox="0 0 8 4">
|
||||
<!-- One tooth and one hole of the stamp's edge. Hole radius 2.5 on a pitch of 8:
|
||||
the mark's 2.2 / 7.03. The hairline is the mark's 0.7 stroke at this scale. -->
|
||||
<path d="M0 0H8V3H6.5A2.5 2.5 0 0 0 1.5 3H0Z" fill="#F3EDE1"/>
|
||||
<path d="M0 3H1.5A2.5 2.5 0 0 1 6.5 3H8" fill="none" stroke="#12161C" stroke-width="0.75"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 409 B |
@@ -0,0 +1,124 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en" data-theme="light">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>Preflight · SelfPost</title>
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bulma@1.0.4/css/bulma.min.css">
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@tabler/icons-webfont@3/dist/tabler-icons.min.css">
|
||||
<link rel="stylesheet" href="../shared/brand.css">
|
||||
<link rel="stylesheet" href="theme.css">
|
||||
</head>
|
||||
<body>
|
||||
<nav class="navbar is-primary" aria-label="main navigation">
|
||||
<div class="container">
|
||||
<div class="navbar-brand"><a class="navbar-item" href="overview.html"><img src="../../selfpost-wordmark.svg" alt="SelfPost"></a></div>
|
||||
<div class="navbar-menu is-active">
|
||||
<div class="navbar-start">
|
||||
<a class="navbar-item" href="overview.html">Overview</a>
|
||||
<div class="navbar-item has-dropdown is-hoverable">
|
||||
<a class="navbar-link" href="out-domains.html">Outbound</a>
|
||||
<div class="navbar-dropdown">
|
||||
<a class="navbar-item" href="out-domains.html"><i class="ti ti-world-upload"></i>Domains</a>
|
||||
<a class="navbar-item" href="out-log.html"><i class="ti ti-list-details"></i>Log</a>
|
||||
<a class="navbar-item" href="out-queue.html"><i class="ti ti-stack-2"></i>Queue</a>
|
||||
<a class="navbar-item" href="dmarc.html"><i class="ti ti-report-analytics"></i>DMARC reports</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="navbar-item has-dropdown is-hoverable">
|
||||
<a class="navbar-link" href="in-domains.html">Inbound</a>
|
||||
<div class="navbar-dropdown">
|
||||
<a class="navbar-item" href="in-domains.html"><i class="ti ti-world-download"></i>Domains</a>
|
||||
<a class="navbar-item" href="in-log.html"><i class="ti ti-list-details"></i>Log</a>
|
||||
<a class="navbar-item" href="in-queue.html"><i class="ti ti-stack-2"></i>Queue</a>
|
||||
<a class="navbar-item" href="in-quarantine.html"><i class="ti ti-mail-pause"></i>Quarantine</a>
|
||||
<a class="navbar-item" href="in-filter-lists.html"><i class="ti ti-filter"></i>Filter lists</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="navbar-item has-dropdown is-hoverable sp-current">
|
||||
<a class="navbar-link" href="health.html">Server</a>
|
||||
<div class="navbar-dropdown">
|
||||
<a class="navbar-item" href="health.html"><i class="ti ti-heartbeat"></i>Health</a>
|
||||
<a class="navbar-item is-active" href="preflight.html"><i class="ti ti-checklist"></i>Preflight</a>
|
||||
<a class="navbar-item" href="system-log.html"><i class="ti ti-file-text"></i>System log</a>
|
||||
<a class="navbar-item" href="backup.html"><i class="ti ti-archive"></i>Backup</a>
|
||||
<a class="navbar-item" href="users.html"><i class="ti ti-users"></i>Users</a>
|
||||
<a class="navbar-item" href="settings.html"><i class="ti ti-settings"></i>Settings</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="navbar-end">
|
||||
<div class="navbar-item has-dropdown is-hoverable">
|
||||
<a class="navbar-link" href="account.html"><i class="ti ti-user-circle"></i>admin</a>
|
||||
<div class="navbar-dropdown is-right">
|
||||
<a class="navbar-item" href="account.html"><i class="ti ti-key"></i>Account</a>
|
||||
<a class="navbar-item" href="help.html"><i class="ti ti-help"></i>Help</a>
|
||||
<hr class="navbar-divider">
|
||||
<form method="post" action="login.html"><button type="submit" class="navbar-item"><i class="ti ti-logout"></i>Sign out</button></form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
<div class="sp-perf" aria-hidden="true"></div>
|
||||
<div class="sp-subnav"><div class="container"><a href="health.html"><i class="ti ti-heartbeat"></i>Health</a><a href="preflight.html" aria-current="page"><i class="ti ti-checklist"></i>Preflight</a><a href="system-log.html"><i class="ti ti-file-text"></i>System log</a><a href="backup.html"><i class="ti ti-archive"></i>Backup</a><a href="users.html"><i class="ti ti-users"></i>Users</a><a href="settings.html"><i class="ti ti-settings"></i>Settings</a></div></div>
|
||||
<main>
|
||||
<div class="container sp-page">
|
||||
<div class="sp-head">
|
||||
<div class="sp-postmark sp-warn"><span>PREFLIGHT</span><b>WARN</b><span>1 OF 7 CHECKS</span></div>
|
||||
<div class="sp-grow">
|
||||
<p class="sp-kicker">Server</p>
|
||||
<h1 class="title is-3">Preflight</h1>
|
||||
<p class="sp-lead">Is this installation ready to deliver mail? Run it after the first start, after moving the server, or when receivers start refusing you. Health watches what runs; Preflight checks what the outside world sees.</p>
|
||||
</div>
|
||||
<form method="post" action="#"><button type="submit" class="button"><i class="ti ti-refresh"></i>Run again</button></form>
|
||||
</div>
|
||||
|
||||
<div class="box">
|
||||
<div class="sp-box-head"><span class="sp-no">01</span><h2>Checks</h2><div class="sp-end">Ran 14:02 UTC · 3.1 s</div></div>
|
||||
<div class="table-container"><table class="table is-fullwidth">
|
||||
<thead><tr><th>Check</th><th>Result</th><th>Found</th><th>What to do</th></tr></thead>
|
||||
<tbody>
|
||||
<tr><td class="sp-nowrap"><strong>Reverse DNS</strong></td><td><span class="tag is-success is-light">ok</span></td><td class="sp-mono sp-small">203.0.113.25 ⇄ mail.example.org</td><td class="sp-muted">—</td></tr>
|
||||
<tr><td class="sp-nowrap"><strong>TLS certificate</strong></td><td><span class="tag is-warning is-light">warn</span></td><td class="sp-small">Valid for <span class="sp-mono">mail.example.org</span>, expires in 12 days</td><td>Renewal has not run. Check that port 80 reaches the ACME client and look for <code>acme</code> in the <a href="system-log.html">system log</a>.</td></tr>
|
||||
<tr><td class="sp-nowrap"><strong>Port 25</strong></td><td><span class="tag is-success is-light">ok</span></td><td class="sp-mono sp-small">220 mail.example.org ESMTP</td><td class="sp-muted">—</td></tr>
|
||||
<tr><td class="sp-nowrap"><strong>Ports 465, 587</strong></td><td><span class="tag is-success is-light">ok</span></td><td class="sp-mono sp-small">TLS handshake, 220 banner on both</td><td class="sp-muted">—</td></tr>
|
||||
<tr><td class="sp-nowrap"><strong>HELO name</strong></td><td><span class="tag is-success is-light">ok</span></td><td class="sp-mono sp-small">mail.example.org</td><td class="sp-muted">—</td></tr>
|
||||
<tr><td class="sp-nowrap"><strong>Proxy headers</strong></td><td><span class="tag is-success is-light">ok</span></td><td class="sp-small">Peer <span class="sp-mono">172.18.0.2</span> is trusted; <span class="sp-mono">X-Forwarded-Proto: https</span></td><td class="sp-muted">—</td></tr>
|
||||
<tr><td class="sp-nowrap"><strong>DKIM signing</strong></td><td><span class="tag is-success is-light">ok</span></td><td class="sp-mono sp-small">inet:127.0.0.1:8891 answers</td><td class="sp-muted">—</td></tr>
|
||||
</tbody>
|
||||
</table></div>
|
||||
<div class="sp-box-foot"><span>Ports are dialled from inside the container at the public address; a firewall that treats inside and outside differently can make this pass while the world is still blocked. SPF, DKIM and DMARC are per domain — each <a href=\"out-domains.html\">outbound domain</a> shows its own.</span></div>
|
||||
</div>
|
||||
|
||||
<div class="box">
|
||||
<div class="sp-box-head"><span class="sp-no">02</span><h2>Send a test e-mail</h2><div class="sp-end">Confirms the relay accepted it, not that it arrived</div></div>
|
||||
<div class="sp-box-body">
|
||||
<div class="columns">
|
||||
<div class="column is-6">
|
||||
<form class="sp-form" method="post" action="#">
|
||||
<div class="field"><label class="label" for="rcpt">Recipient</label>
|
||||
<div class="field has-addons">
|
||||
<div class="control is-expanded has-icons-left"><input class="input" id="rcpt" name="recipient" type="email" placeholder="you@elsewhere.example" required><span class="icon is-left"><i class="ti ti-mail"></i></span></div>
|
||||
<div class="control"><button type="submit" class="button is-primary"><i class="ti ti-send"></i>Send</button></div>
|
||||
</div>
|
||||
<p class="help">Sent from <span class="sp-mono">postmaster@mail.example.org</span> through the local Postfix.</p></div>
|
||||
</form>
|
||||
</div>
|
||||
<div class="column is-6">
|
||||
<dl class="sp-facts">
|
||||
<div><dt>Last test</dt><dd>14:03 UTC to <span class="sp-mono">mix@elsewhere.example</span></dd></div>
|
||||
<div><dt>Result</dt><dd><span class="tag is-success is-light">queued</span> <span class="sp-mono sp-small">4XcC1d8Pq2z1</span></dd></div>
|
||||
<div><dt>Then</dt><dd><a href="out-message.html">Follow it in the outbound log</a></dd></div>
|
||||
</dl>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<footer class="sp-foot">SelfPost 1.8.0 · © 2026 Mix · <a href="#">License (AGPL-3.0)</a> · <a href="#">Source</a> · No warranty</footer>
|
||||
</div>
|
||||
</main>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,108 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en" data-theme="light">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>Settings · SelfPost</title>
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bulma@1.0.4/css/bulma.min.css">
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@tabler/icons-webfont@3/dist/tabler-icons.min.css">
|
||||
<link rel="stylesheet" href="../shared/brand.css">
|
||||
<link rel="stylesheet" href="theme.css">
|
||||
</head>
|
||||
<body>
|
||||
<nav class="navbar is-primary" aria-label="main navigation">
|
||||
<div class="container">
|
||||
<div class="navbar-brand"><a class="navbar-item" href="overview.html"><img src="../../selfpost-wordmark.svg" alt="SelfPost"></a></div>
|
||||
<div class="navbar-menu is-active">
|
||||
<div class="navbar-start">
|
||||
<a class="navbar-item" href="overview.html">Overview</a>
|
||||
<div class="navbar-item has-dropdown is-hoverable">
|
||||
<a class="navbar-link" href="out-domains.html">Outbound</a>
|
||||
<div class="navbar-dropdown">
|
||||
<a class="navbar-item" href="out-domains.html"><i class="ti ti-world-upload"></i>Domains</a>
|
||||
<a class="navbar-item" href="out-log.html"><i class="ti ti-list-details"></i>Log</a>
|
||||
<a class="navbar-item" href="out-queue.html"><i class="ti ti-stack-2"></i>Queue</a>
|
||||
<a class="navbar-item" href="dmarc.html"><i class="ti ti-report-analytics"></i>DMARC reports</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="navbar-item has-dropdown is-hoverable">
|
||||
<a class="navbar-link" href="in-domains.html">Inbound</a>
|
||||
<div class="navbar-dropdown">
|
||||
<a class="navbar-item" href="in-domains.html"><i class="ti ti-world-download"></i>Domains</a>
|
||||
<a class="navbar-item" href="in-log.html"><i class="ti ti-list-details"></i>Log</a>
|
||||
<a class="navbar-item" href="in-queue.html"><i class="ti ti-stack-2"></i>Queue</a>
|
||||
<a class="navbar-item" href="in-quarantine.html"><i class="ti ti-mail-pause"></i>Quarantine</a>
|
||||
<a class="navbar-item" href="in-filter-lists.html"><i class="ti ti-filter"></i>Filter lists</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="navbar-item has-dropdown is-hoverable sp-current">
|
||||
<a class="navbar-link" href="health.html">Server</a>
|
||||
<div class="navbar-dropdown">
|
||||
<a class="navbar-item" href="health.html"><i class="ti ti-heartbeat"></i>Health</a>
|
||||
<a class="navbar-item" href="preflight.html"><i class="ti ti-checklist"></i>Preflight</a>
|
||||
<a class="navbar-item" href="system-log.html"><i class="ti ti-file-text"></i>System log</a>
|
||||
<a class="navbar-item" href="backup.html"><i class="ti ti-archive"></i>Backup</a>
|
||||
<a class="navbar-item" href="users.html"><i class="ti ti-users"></i>Users</a>
|
||||
<a class="navbar-item is-active" href="settings.html"><i class="ti ti-settings"></i>Settings</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="navbar-end">
|
||||
<div class="navbar-item has-dropdown is-hoverable">
|
||||
<a class="navbar-link" href="account.html"><i class="ti ti-user-circle"></i>admin</a>
|
||||
<div class="navbar-dropdown is-right">
|
||||
<a class="navbar-item" href="account.html"><i class="ti ti-key"></i>Account</a>
|
||||
<a class="navbar-item" href="help.html"><i class="ti ti-help"></i>Help</a>
|
||||
<hr class="navbar-divider">
|
||||
<form method="post" action="login.html"><button type="submit" class="navbar-item"><i class="ti ti-logout"></i>Sign out</button></form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
<div class="sp-perf" aria-hidden="true"></div>
|
||||
<div class="sp-subnav"><div class="container"><a href="health.html"><i class="ti ti-heartbeat"></i>Health</a><a href="preflight.html"><i class="ti ti-checklist"></i>Preflight</a><a href="system-log.html"><i class="ti ti-file-text"></i>System log</a><a href="backup.html"><i class="ti ti-archive"></i>Backup</a><a href="users.html"><i class="ti ti-users"></i>Users</a><a href="settings.html" aria-current="page"><i class="ti ti-settings"></i>Settings</a></div></div>
|
||||
<main>
|
||||
<div class="container sp-page">
|
||||
<div class="sp-head">
|
||||
<div class="sp-grow">
|
||||
<p class="sp-kicker">Server</p>
|
||||
<h1 class="title is-3">Settings</h1>
|
||||
<p class="sp-lead">What holds for the whole instance, whoever is signed in. Your own name, e-mail and password are under <a href="account.html">Account</a>.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<form class="sp-form" method="post" action="#">
|
||||
<div class="columns">
|
||||
<div class="column is-6">
|
||||
<div class="box">
|
||||
<div class="sp-box-head"><span class="sp-no">01</span><h2>Log retention</h2><a class="sp-help" href="help.html#settings" title="How long rows are kept"><i class="ti ti-help-circle"></i></a></div>
|
||||
<div class="sp-box-body">
|
||||
<div class="field"><label class="label" for="ret">Keep outbound log rows, days</label>
|
||||
<div class="control"><input class="input" id="ret" name="send_log_retention_days" type="number" min="7" max="365" value="30" required></div>
|
||||
<p class="help">The main driver of <code>/data</code> growth. Does not affect <code>mail.log</code> rotation (fourteen daily files).</p></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="column is-6">
|
||||
<div class="box" id="rate-limits">
|
||||
<div class="sp-box-head"><span class="sp-no">02</span><h2>Sending rate limits</h2><div class="sp-end">Read-only</div><a class="sp-help" href="help.html#limits" title="Levels 1 and 2"><i class="ti ti-help-circle"></i></a></div>
|
||||
<div class="sp-box-body">
|
||||
<dl class="sp-facts">
|
||||
<div><dt>Level 1 · per client IP</dt><dd class="sp-big">600 / h</dd><dd class="sp-muted">Set in <code>.env</code>; restart to change. Nothing below may exceed it.</dd></div>
|
||||
<div><dt>Level 2 · domain, application</dt><dd>Set on each <a href="out-domain-settings.html">domain</a> and <a href="out-app.html">application</a>.</dd></div>
|
||||
</dl>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="buttons"><button type="submit" class="button is-primary">Save settings</button></div>
|
||||
</form>
|
||||
|
||||
<footer class="sp-foot">SelfPost 1.8.0 · © 2026 Mix · <a href="#">License (AGPL-3.0)</a> · <a href="#">Source</a> · No warranty</footer>
|
||||
</div>
|
||||
</main>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,44 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en" data-theme="light">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>Create administrator · SelfPost</title>
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bulma@1.0.4/css/bulma.min.css">
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@tabler/icons-webfont@3/dist/tabler-icons.min.css">
|
||||
<link rel="stylesheet" href="../shared/brand.css">
|
||||
<link rel="stylesheet" href="theme.css">
|
||||
</head>
|
||||
<body>
|
||||
<div class="columns is-gapless sp-signin">
|
||||
<div class="column is-half sp-signin-brand">
|
||||
<div>
|
||||
<img src="../../selfpost-stamp.svg" alt="SelfPost">
|
||||
<p>YOUR SMTP. YOUR RULES.</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="column is-half sp-signin-form">
|
||||
<form class="sp-form" method="post" action="overview.html">
|
||||
<p class="sp-kicker">First run · one-time link</p>
|
||||
<h1 class="title is-3">Create administrator</h1>
|
||||
<p class="sp-muted mb-4">This link creates the single global administrator. After you submit, it stops working for good.</p>
|
||||
<div class="field">
|
||||
<label class="label" for="username">Username</label>
|
||||
<div class="control has-icons-left"><input class="input" id="username" name="username" autocomplete="username" required><span class="icon is-left"><i class="ti ti-user"></i></span></div>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label class="label" for="password">Password</label>
|
||||
<div class="control has-icons-left"><input class="input" id="password" name="password" type="password" autocomplete="new-password" required><span class="icon is-left"><i class="ti ti-lock"></i></span></div>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label class="label" for="password_confirm">Confirm password</label>
|
||||
<div class="control has-icons-left"><input class="input" id="password_confirm" name="password_confirm" type="password" autocomplete="new-password" required><span class="icon is-left"><i class="ti ti-lock"></i></span></div>
|
||||
</div>
|
||||
<div class="field mt-5"><button type="submit" class="button is-primary is-fullwidth">Create administrator</button></div>
|
||||
</form>
|
||||
<p class="sp-foot">© 2026 Mix · <a href="#">License (AGPL-3.0)</a> · <a href="#">Source</a> · No warranty</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,41 @@
|
||||
<!-- title: Account | section: user -->
|
||||
<div class="sp-head">
|
||||
<div class="sp-grow">
|
||||
<p class="sp-kicker">Signed in as admin · global</p>
|
||||
<h1 class="title is-3">Account</h1>
|
||||
<p class="sp-lead">Who you are to this panel. Applications keep their own logins and passwords and are not affected.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="columns">
|
||||
<div class="column is-6">
|
||||
<div class="box">
|
||||
<div class="sp-box-head"><span class="sp-no">01</span><h2>Profile</h2></div>
|
||||
<div class="sp-box-body">
|
||||
<form class="sp-form" method="post" action="#">
|
||||
<div class="field"><label class="label" for="username">Username</label><div class="control has-icons-left"><input class="input" id="username" name="username" value="admin" autocomplete="username" spellcheck="false" required><span class="icon is-left"><i class="ti ti-user"></i></span></div></div>
|
||||
<div class="field"><label class="label" for="email">E-mail</label><div class="control has-icons-left"><input class="input" id="email" name="email" type="email" value="mix@example.org" autocomplete="email"><span class="icon is-left"><i class="ti ti-mail"></i></span></div>
|
||||
<p class="help">Your own address. A domain's report address can be filled with it from Domain settings.</p></div>
|
||||
<button type="submit" class="button is-primary">Save profile</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="column is-6">
|
||||
<div class="box">
|
||||
<div class="sp-box-head"><span class="sp-no">02</span><h2>Password</h2></div>
|
||||
<div class="sp-box-body">
|
||||
<form class="sp-form" method="post" action="#">
|
||||
<div class="field"><label class="label" for="cur">Current password</label><div class="control"><input class="input" id="cur" name="current_password" type="password" autocomplete="current-password" required></div></div>
|
||||
<div class="sp-pair">
|
||||
<div class="field"><label class="label" for="new">New password</label><div class="control"><input class="input" id="new" name="new_password" type="password" autocomplete="new-password" required></div></div>
|
||||
<div class="field"><label class="label" for="new2">Repeat</label><div class="control"><input class="input" id="new2" name="new_password_confirm" type="password" autocomplete="new-password" required></div></div>
|
||||
</div>
|
||||
<p class="help mb-3">Changing it signs out every other session of this account; this one stays signed in.</p>
|
||||
<button type="submit" class="button">Change password</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,44 @@
|
||||
<!-- title: Backup | section: server | page: backup -->
|
||||
<div class="sp-head">
|
||||
<div class="sp-grow">
|
||||
<p class="sp-kicker">Server</p>
|
||||
<h1 class="title is-3">Backup</h1>
|
||||
<p class="sp-lead">Take the whole instance with you, or bring one domain in from another SelfPost. Both files are secrets.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="columns">
|
||||
<div class="column is-6">
|
||||
<div class="box">
|
||||
<div class="sp-box-head"><span class="sp-no">01</span><h2>Full backup</h2><a class="sp-help" href="help.html#backup" title="Backups, encryption and restore"><i class="ti ti-help-circle"></i></a></div>
|
||||
<div class="sp-box-body">
|
||||
<p class="mb-3"><code>data/</code> (database, DKIM keys, credentials, the queue), <code>docker-compose.yml</code>, <code>.env</code> and <code>certs/</code> in one archive. Restore into an empty project directory and start the <strong>same SelfPost version</strong>. The reverse-proxy vhost is not included.</p>
|
||||
<form class="sp-form" method="post" action="#">
|
||||
<div class="field"><label class="checkbox"><input type="checkbox" name="encrypt" checked> Encrypt with a password (<code>.spbk</code>)</label></div>
|
||||
<div class="sp-pair">
|
||||
<div class="field"><label class="label" for="pw">Password</label><div class="control"><input class="input" id="pw" type="password" placeholder="at least 12 characters"></div></div>
|
||||
<div class="field"><label class="label" for="pw2">Repeat password</label><div class="control"><input class="input" id="pw2" type="password"></div></div>
|
||||
</div>
|
||||
<p class="help mb-3">Keep this password: SelfPost does not store it, and the file cannot be opened without it.</p>
|
||||
<button type="submit" class="button is-primary"><i class="ti ti-download"></i>Download full backup</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="column is-6">
|
||||
<div class="box" id="import">
|
||||
<div class="sp-box-head"><span class="sp-no">02</span><h2>Import a domain</h2><a class="sp-help" href="help.html#backup" title="Moving a domain to another instance"><i class="ti ti-help-circle"></i></a></div>
|
||||
<div class="sp-box-body">
|
||||
<p class="mb-3">A domain export from another instance — plain <code>.json</code> or encrypted <code>.spde</code>. Its DKIM key and application passwords come across, so the published DNS needs no change. The export itself is made on the <a href="out-domain-settings.html">domain's settings</a>.</p>
|
||||
<form class="sp-form" method="post" action="out-domain.html" enctype="multipart/form-data">
|
||||
<div class="field"><label class="label">Domain export file</label>
|
||||
<div class="file has-name is-fullwidth"><label class="file-label"><input class="file-input" type="file" name="file" accept=".json,.spde"><span class="file-cta"><i class="ti ti-upload"></i><span class="file-label ml-2">Choose a file…</span></span><span class="file-name">example.org.spde</span></label></div></div>
|
||||
<div class="field"><label class="label" for="ipw">Password</label><div class="control"><input class="input" id="ipw" name="import_password" type="password" autocomplete="off"></div>
|
||||
<p class="help">Asked only for an encrypted <code>.spde</code> file.</p></div>
|
||||
<button type="submit" class="button is-primary"><i class="ti ti-file-import"></i>Import domain</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||