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,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
|
# 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
|
# 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
|
# 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
|
# bytes that passed e2e. The per-arch tags stay in GHCR next to the version
|
||||||
# assemble the multi-arch manifest; merge removes them from GHCR so operators
|
# tag: they are the images the multi-arch manifest points at, and GHCR cannot
|
||||||
# see a single version tag.
|
# drop a tag without deleting the image behind it.
|
||||||
on:
|
on:
|
||||||
release:
|
release:
|
||||||
types: [published]
|
types: [published]
|
||||||
@@ -177,28 +177,35 @@ jobs:
|
|||||||
backoff=$((backoff * 2))
|
backoff=$((backoff * 2))
|
||||||
done
|
done
|
||||||
|
|
||||||
- name: Remove per-arch tags from GHCR
|
- name: Remove orphaned untagged versions from GHCR
|
||||||
# Side-effect tags for imagetools assembly only — not part of the public
|
# Re-publishing a version moves its tags to new manifests and leaves the
|
||||||
# version surface (deploy/docker-compose.yml pins X.Y.Z, not X.Y.Z-amd64).
|
# old ones behind as untagged "sha256:..." package versions. Delete only
|
||||||
# imagetools has no "rm" subcommand; delete via the GitHub Packages API.
|
# 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:
|
env:
|
||||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
run: |
|
run: |
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
version="${{ needs.prepare.outputs.version }}"
|
image="ghcr.io/${{ github.repository }}"
|
||||||
owner="${{ github.repository_owner }}"
|
owner="${{ github.repository_owner }}"
|
||||||
pkg="${{ github.event.repository.name }}"
|
pkg="${{ github.event.repository.name }}"
|
||||||
api="/users/${owner}/packages/container/${pkg}/versions"
|
api="/users/${owner}/packages/container/${pkg}/versions"
|
||||||
for suffix in amd64 arm64; do
|
versions=$(gh api "${api}?per_page=100" --paginate \
|
||||||
tag="${version}-${suffix}"
|
--jq '.[] | [.id, .name, (.metadata.container.tags | length)] | @tsv')
|
||||||
mapfile -t ids < <(gh api "$api" --paginate \
|
keep=$(mktemp)
|
||||||
--jq ".[] | select([.metadata.container.tags[]] | index(\"${tag}\")) | .id")
|
while IFS=$'\t' read -r id digest ntags; do
|
||||||
if [ "${#ids[@]}" -eq 0 ]; then
|
[ "$ntags" -gt 0 ] || continue
|
||||||
echo "no GHCR package version for tag ${tag}"
|
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
|
continue
|
||||||
fi
|
fi
|
||||||
for id in "${ids[@]}"; do
|
echo "deleting orphaned GHCR package version ${id} (${digest})"
|
||||||
echo "deleting GHCR package version ${id} (tag ${tag})"
|
|
||||||
gh api -X DELETE "${api}/${id}"
|
gh api -X DELETE "${api}/${id}"
|
||||||
done
|
done <<<"$versions"
|
||||||
done
|
|
||||||
|
|||||||
@@ -10,6 +10,15 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- 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
|
- uses: actions/setup-go@v5
|
||||||
with:
|
with:
|
||||||
|
|||||||
@@ -5,6 +5,334 @@ Format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/); version
|
|||||||
|
|
||||||
## [Unreleased]
|
## [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
|
## [1.9.3] - 2026-09-22
|
||||||
|
|
||||||
Panel-redesign prep, docs only: stage 1-2 mockups on all 33 screens, the
|
Panel-redesign prep, docs only: stage 1-2 mockups on all 33 screens, the
|
||||||
|
|||||||
@@ -21,6 +21,22 @@ htmx 2.0.4 (internal/web/view/static/htmx.min.js)
|
|||||||
Licence: Zero-Clause BSD (0BSD)
|
Licence: Zero-Clause BSD (0BSD)
|
||||||
https://github.com/bigskysoftware/htmx
|
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
|
IBM Plex Sans / IBM Plex Mono
|
||||||
Copyright © 2017 IBM Corp., with Reserved Font Name "Plex"
|
Copyright © 2017 IBM Corp., with Reserved Font Name "Plex"
|
||||||
Licence: SIL Open Font License 1.1
|
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
|
- 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 inbound relay on port 25 (backup-MX / forwarder; off by default)
|
||||||
- Optional DMARC aggregate report ingest on port 25 (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)
|
- 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
|
- Multi-domain relay — each SASL application is bound to one sending domain
|
||||||
- DNS status checks (PTR, SPF, DKIM, DMARC) with in-panel re-check
|
- 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
|
- 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 SELFPOST_HOSTNAME=mail.local.test \
|
||||||
-e PANEL_COOKIE_SECURE=false \
|
-e PANEL_COOKIE_SECURE=false \
|
||||||
-v selfpost-try-data:/data \
|
-v selfpost-try-data:/data \
|
||||||
ghcr.io/mixeme/selfpost:1.9.3
|
ghcr.io/mixeme/selfpost:2.0.0
|
||||||
```
|
```
|
||||||
|
|
||||||
**Get the setup URL** (pick one):
|
**Get the setup URL** (pick one):
|
||||||
|
|||||||
@@ -157,5 +157,16 @@ if [ ! -d /data/postfix/queue/active ]; then
|
|||||||
postfix set-permissions
|
postfix set-permissions
|
||||||
fi
|
fi
|
||||||
chown -R postfix:postfix /data/postfix/queue
|
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
|
exec /usr/bin/supervisord -c /etc/supervisor/supervisord.conf
|
||||||
|
|||||||
@@ -13,12 +13,25 @@
|
|||||||
# a fixed literal or a container environment variable.
|
# a fixed literal or a container environment variable.
|
||||||
set -eu
|
set -eu
|
||||||
|
|
||||||
# postconf -P refuses master.cf -o values that contain whitespace; inbound port
|
# Sets one -o option of the smtp/inet (port 25) service by patching its stanza.
|
||||||
# 25 needs restriction lists and milter specs that include spaces. Patch the
|
#
|
||||||
# smtp/inet service stanza directly instead.
|
# 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() {
|
master_cf_set_smtp_inet_o() {
|
||||||
param="$1"
|
param="$1"
|
||||||
value="$2"
|
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" '
|
awk -v p="$param" -v v="$value" '
|
||||||
/^smtp[ \t]+inet[ \t]/ && !done {
|
/^smtp[ \t]+inet[ \t]/ && !done {
|
||||||
print
|
print
|
||||||
@@ -272,11 +285,21 @@ if [ "${INBOUND_RELAY_ENABLE}" = "true" ] || [ "${DMARC_REPORTS_ENABLE}" = "true
|
|||||||
[ -e "$f" ] || : > "$f"
|
[ -e "$f" ] || : > "$f"
|
||||||
done
|
done
|
||||||
ln -sf /usr/local/bin/panel /usr/local/bin/dmarc-ingest
|
ln -sf /usr/local/bin/panel /usr/local/bin/dmarc-ingest
|
||||||
postconf -M "dmarc-ingest/unix-pipe=dmarc-ingest unix - n n - - pipe"
|
# flags=, user= and argv= are arguments of pipe(8), not -o parameters:
|
||||||
postconf -P \
|
# postconf -P refuses them ("unmatched service_name/type") and leaves a
|
||||||
"dmarc-ingest/unix-pipe/flags=Rq" \
|
# bare "pipe", which dies with "fatal: missing user= command-line
|
||||||
"dmarc-ingest/unix-pipe/user=panel" \
|
# attribute" on every report. They go into the service line itself.
|
||||||
"dmarc-ingest/unix-pipe/argv=/usr/local/bin/dmarc-ingest"
|
# ${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
|
if [ -n "$RELAY_DOMAINS_SETTING" ]; then
|
||||||
RELAY_DOMAINS_SETTING="${RELAY_DOMAINS_SETTING} texthash:${DMARC_RELAY_DOMAINS_MAP}"
|
RELAY_DOMAINS_SETTING="${RELAY_DOMAINS_SETTING} texthash:${DMARC_RELAY_DOMAINS_MAP}"
|
||||||
TRANSPORT_SETTING="${TRANSPORT_SETTING} texthash:${DMARC_TRANSPORT_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}"
|
RELAY_DOMAINS_SETTING="texthash:${DMARC_RELAY_DOMAINS_MAP}"
|
||||||
TRANSPORT_SETTING="texthash:${DMARC_TRANSPORT_MAP}"
|
TRANSPORT_SETTING="texthash:${DMARC_TRANSPORT_MAP}"
|
||||||
fi
|
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
|
fi
|
||||||
postconf -e \
|
postconf -e \
|
||||||
"relay_domains=${RELAY_DOMAINS_SETTING}" \
|
"relay_domains=${RELAY_DOMAINS_SETTING}" \
|
||||||
"transport_maps=${TRANSPORT_SETTING}" \
|
"transport_maps=${TRANSPORT_SETTING}" \
|
||||||
"smtpd_reject_unlisted_recipient=yes"
|
"smtpd_reject_unlisted_recipient=yes"
|
||||||
if [ "${INBOUND_RELAY_ENABLE}" = "true" ]; then
|
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 \
|
postconf -e \
|
||||||
"relay_recipient_maps=texthash:${RELAY_RECIPIENTS_MAP}" \
|
"relay_recipient_maps=${RELAY_RECIPIENT_SETTING}" \
|
||||||
"smtp_tls_policy_maps=texthash:${TLS_POLICY_MAP}"
|
"smtp_tls_policy_maps=texthash:${TLS_POLICY_MAP}"
|
||||||
else
|
else
|
||||||
postconf -e \
|
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}"
|
master_cf_set_smtp_inet_o smtpd_milters "${INBOUND_MILTERS}"
|
||||||
else
|
else
|
||||||
postconf -MX "smtp/inet" 2>/dev/null || true
|
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 \
|
postconf -e \
|
||||||
"relay_domains=" \
|
"relay_domains=" \
|
||||||
"transport_maps=" \
|
"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()
|
defer st.Close()
|
||||||
|
|
||||||
recipient := strings.ToLower(strings.TrimSpace(os.Getenv("RECIPIENT")))
|
recipient := ingestRecipient(os.Args[1:])
|
||||||
if recipient == "" {
|
|
||||||
recipient = strings.ToLower(strings.TrimSpace(os.Getenv("ORIGINAL_RECIPIENT")))
|
|
||||||
}
|
|
||||||
if err := dmarc.IngestMessage(st, io.LimitReader(os.Stdin, maxIngestStdin), recipient, time.Now().UTC()); err != nil {
|
if err := dmarc.IngestMessage(st, io.LimitReader(os.Stdin, maxIngestStdin), recipient, time.Now().UTC()); err != nil {
|
||||||
if incrErr := st.IncrDMARCParseFailures(); incrErr != nil {
|
if incrErr := st.IncrDMARCParseFailures(); incrErr != nil {
|
||||||
log.Printf("dmarc ingest: record failure: %v", incrErr)
|
log.Printf("dmarc ingest: record failure: %v", incrErr)
|
||||||
@@ -39,6 +36,21 @@ func runDMARCIngestMode() error {
|
|||||||
return nil
|
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 {
|
func isDMARCIngestInvocation() bool {
|
||||||
if len(os.Args) > 1 && os.Args[1] == "-dmarc-ingest" {
|
if len(os.Args) > 1 && os.Args[1] == "-dmarc-ingest" {
|
||||||
return true
|
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",
|
"POSTFIX_DIR",
|
||||||
"SELFPOST_DEPLOY_ROOT",
|
"SELFPOST_DEPLOY_ROOT",
|
||||||
"INBOUND_RELAY_ENABLE",
|
"INBOUND_RELAY_ENABLE",
|
||||||
|
"INBOUND_ANTISPAM_MILTER",
|
||||||
|
"INBOUND_ANTISPAM_MILTER_ACTION",
|
||||||
"DMARC_REPORTS_ENABLE",
|
"DMARC_REPORTS_ENABLE",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -110,6 +110,8 @@ func newPanel(cfg config, st *store.Store) (*web.Server, error) {
|
|||||||
RateLimitWindowSeconds: cfg.rateLimitWindowSeconds,
|
RateLimitWindowSeconds: cfg.rateLimitWindowSeconds,
|
||||||
RetryPolicy: retryPolicy,
|
RetryPolicy: retryPolicy,
|
||||||
InboundEnabled: cfg.inboundEnabled,
|
InboundEnabled: cfg.inboundEnabled,
|
||||||
|
InboundAntispamMilter: cfg.inboundAntispamMilter,
|
||||||
|
InboundAntispamAction: cfg.inboundAntispamAction,
|
||||||
DMARCEnabled: cfg.dmarcEnabled,
|
DMARCEnabled: cfg.dmarcEnabled,
|
||||||
SendLogRetentionEnvDefault: cfg.retentionDays,
|
SendLogRetentionEnvDefault: cfg.retentionDays,
|
||||||
}, cfg.setupTokenPath)
|
}, cfg.setupTokenPath)
|
||||||
|
|||||||
@@ -91,6 +91,11 @@ type config struct {
|
|||||||
deployRoot string
|
deployRoot string
|
||||||
|
|
||||||
inboundEnabled bool
|
inboundEnabled 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
|
dmarcEnabled bool
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -155,11 +160,27 @@ func loadConfig() config {
|
|||||||
deployRoot: envDefault("SELFPOST_DEPLOY_ROOT", "/selfpost-deploy"),
|
deployRoot: envDefault("SELFPOST_DEPLOY_ROOT", "/selfpost-deploy"),
|
||||||
// Optional inbound relay (backup-MX / forwarder). Off unless exactly "true".
|
// 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".
|
// Optional DMARC aggregate ingest on port 25. Off unless exactly "true".
|
||||||
dmarcEnabled: os.Getenv("DMARC_REPORTS_ENABLE") == "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
|
// 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
|
// hostname Postfix's SASL layer uses so a client authenticating with a bare
|
||||||
// login resolves to the right account.
|
// login resolves to the right account.
|
||||||
|
|||||||
@@ -74,7 +74,7 @@ func restoreFromOwnBackup(t *testing.T, password string) restored {
|
|||||||
func TestPanelBootsOnADataDirectoryRestoredFromItsOwnBackup(t *testing.T) {
|
func TestPanelBootsOnADataDirectoryRestoredFromItsOwnBackup(t *testing.T) {
|
||||||
r := restoreFromOwnBackup(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} {
|
for _, want := range []string{restoreDomain, restoreSubject} {
|
||||||
if !strings.Contains(body, want) {
|
if !strings.Contains(body, want) {
|
||||||
t.Errorf("the restored panel's send log does not show %q:\n%s", want, body)
|
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) {
|
func TestARestoredPanelHonoursSessionsFromTheArchive(t *testing.T) {
|
||||||
r := restoreFromOwnBackup(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 {
|
if rec.Code != http.StatusOK {
|
||||||
t.Errorf("a session from before the backup = %d on the restored panel, want 200", rec.Code)
|
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) {
|
func TestAnEncryptedBackupRestoresTheSameWay(t *testing.T) {
|
||||||
r := restoreFromOwnBackup(t, "a-long-enough-password")
|
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) {
|
if !strings.Contains(body, restoreSubject) {
|
||||||
t.Errorf("the panel restored from an encrypted backup lost the send log:\n%s", body)
|
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", password)
|
||||||
form.Set("password_confirm", 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 {
|
if rec.Code != http.StatusOK {
|
||||||
t.Fatalf("download a backup = %d, want 200:\n%s", rec.Code, rec.Body.String())
|
t.Fatalf("download a backup = %d, want 200:\n%s", rec.Code, rec.Body.String())
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -21,7 +21,7 @@
|
|||||||
|
|
||||||
services:
|
services:
|
||||||
selfpost:
|
selfpost:
|
||||||
image: ghcr.io/mixeme/selfpost:1.9.3
|
image: ghcr.io/mixeme/selfpost:2.0.0
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
environment:
|
environment:
|
||||||
SELFPOST_HOSTNAME: "${SELFPOST_HOSTNAME:?set the mail/panel hostname, e.g. mail.example.com}"
|
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`,
|
parameters (`queue_run_delay`, `minimal_backoff_time`,
|
||||||
`maximal_backoff_time`, `maximal_queue_lifetime`, `bounce_queue_lifetime`,
|
`maximal_backoff_time`, `maximal_queue_lifetime`, `bounce_queue_lifetime`,
|
||||||
`delay_warning_time`) and caches the snapshot on the handlers config. The
|
`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,
|
numbers; they never call `postconf` per request. If `postconf` is missing,
|
||||||
the panel logs a warning and uses Postfix 3.x compiled-in defaults
|
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/
|
2. **journal-milter** — unix socket `JOURNAL_MILTER_SOCKET`; records From/To/
|
||||||
Subject/SASL user at DATA; enforces level-2 rate limits; **fail-open**
|
Subject/SASL user at DATA; enforces level-2 rate limits; **fail-open**
|
||||||
(`default_action=accept`) so milter failure does not stop mail. Domain
|
(`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
|
3. **log-tailer** — follows `MAIL_LOG`, updates send-log delivery status by
|
||||||
queue-id. Send-log `queued → sent` transitions depend on this goroutine alone
|
queue-id. Send-log `queued → sent` transitions depend on this goroutine alone
|
||||||
(`UpdateStatus` is only called from [internal/logtail](../internal/logtail/logtail.go)).
|
(`UpdateStatus` is only called from [internal/logtail](../internal/logtail/logtail.go)).
|
||||||
4. **rate-limit recalc** — every six hours (and on demand from the domain
|
4. **rate-limit recalc** — every six hours (and on demand from Domain settings and
|
||||||
page), recomputes level-2 **Auto** rate limits from send-log statistics.
|
the application form), recomputes level-2 **Auto** rate limits from send-log statistics.
|
||||||
|
|
||||||
Milter chain in Postfix: OpenDKIM (tempfail) then journal (accept on failure).
|
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
|
switching descriptors; the panel treats a missing log file as an empty tail, not
|
||||||
an error.
|
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
|
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
|
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
|
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
|
**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
|
serving panel pages on request: `TailLines` (the last *n* lines, for
|
||||||
`/system-log`) and `QueueLines` (the lines carrying one queue-id, for
|
`/server/log`) and `QueueLines` (the lines carrying one queue-id, for
|
||||||
`/deliveries/{id}`). `QueueLines` scans a bounded tail of the current file —
|
`/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
|
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
|
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
|
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
|
## Panel HTTP surface
|
||||||
|
|
||||||
Canonical routes: [internal/web/web.go](../internal/web/web.go). Authenticated
|
Canonical routes: [internal/web/web.go](../internal/web/web.go). The router is
|
||||||
unless noted. Routes marked **global** return **404** for domain administrators
|
two flat `http.ServeMux`es built by `muxes()` — one pattern per route, method
|
||||||
(`requireGlobal()` in
|
included (`GET /outbound/log/{id}`), nothing mounted as a sub-router, and
|
||||||
[internal/web/handlers/authz.go](../internal/web/handlers/authz.go)). The table
|
building them only takes method values, so the route guard test can run it on
|
||||||
below is a summary — HTMX fragment endpoints
|
a bare `Server` and ask which pattern answers a path:
|
||||||
(`/status/fragment`, `/deliveries/rows`, `/mail-queue/body`,
|
|
||||||
`/system-log/body`, …) and every POST variant live in `web.go`.
|
|
||||||
|
|
||||||
| 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) |
|
| 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) |
|
||||||
| `/license` | Embedded `LICENSE` text (no auth) |
|
| Overview | `GET /overview`, `GET /overview/fragment` |
|
||||||
| `/setup/*` | One-time admin bootstrap |
|
| Outbound › Domains | `GET, POST /outbound/domains` · `/outbound/domains/{id}` · `POST …/dns-recheck` · `GET, POST …/delete` · `GET …/settings` · `POST …/settings/reports`, `…/ratelimit`, `…/ratelimit/recalc`, `…/export` |
|
||||||
| `/login`, `/logout` | Session auth |
|
| Outbound › Applications | `GET, POST /outbound/domains/{id}/applications/new` · `GET, POST …/applications/{aid}` · `POST …/applications/{aid}/password`, `…/{aid}/ratelimit/recalc`, `…/{aid}/delete` |
|
||||||
| `/account` | 308 redirect to `/settings` (pre-1.2.3 route, kept as a compat shim) |
|
| Outbound › Log, Queue | `GET /outbound/log` · `/outbound/log/fragment` · `/outbound/log/{id}` · `GET /outbound/queue` · `/outbound/queue/fragment` |
|
||||||
| `/status`, `/status/*` | **Global.** Process, cert, socket, PTR checks; machine CPU/memory/network |
|
| Outbound › DMARC reports | `GET /outbound/dmarc` · `…/dmarc/domains/{id}` · `…/dmarc/reports/{id}` — registered only with `DMARC_REPORTS_ENABLE=true` |
|
||||||
| `/domains` | Domain list; `POST /domains` (add domain) is **global** |
|
| Inbound › Domains | `GET, POST /inbound/domains` · `/inbound/domains/{id}` · `POST …/dns-recheck`, `…/upstream`, `…/recipients` · `GET, POST …/delete` — registered only with `INBOUND_RELAY_ENABLE=true` |
|
||||||
| `/domains/{id}`, `/domains/{id}/*` | Assigned-domain detail for domain-admins; delete domain is **global** |
|
| 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` |
|
||||||
| `/domains/import` | **Global.** Domain import (`POST`; form on the Backup page) |
|
| Account | `GET /account` · `POST /account/profile`, `/account/password` |
|
||||||
| `/deliveries`, `/deliveries/{id}` | Send log with filters; scoped to assigned domains for domain-admins |
|
| User menu | `GET /help` · `POST /logout` (public mux) |
|
||||||
| `/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. |
|
|
||||||
|
|
||||||
HTMX polling refreshes monitoring fragments (5 s while the operator is active on
|
An application is addressed under its domain, and an application id that does
|
||||||
the page, 30 s when the tab is visible but idle, none when hidden — scheduled in
|
not belong to the domain in the path is a 404. The application form is one
|
||||||
`panel.js` via `data-poll`, not `hx-trigger="every …"`); polling does not extend
|
`POST`: sender, client IPs and rate limit are validated and saved together or
|
||||||
session idle timeout (only non-`HX-Request` GET and mutating requests count as
|
not at all, also at creation. The page that shows a new password is the
|
||||||
activity). The Mail queue retry-policy card is outside that fragment: it is the
|
response to the `POST` that created or regenerated it; it has no `GET` path and
|
||||||
start-up `postconf -h` snapshot (see [Panel binary](#panel-binary-cmdpanel)),
|
carries `Cache-Control: no-store`.
|
||||||
not a live re-read.
|
|
||||||
|
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
|
### Sessions
|
||||||
|
|
||||||
Stored in SQLite (`sessions` table, migration `0002`): cookie holds a random
|
Stored in SQLite (`sessions` table of the baseline, `0001_init.sql`): the cookie
|
||||||
token; the database stores **SHA-256 of the token**, not the token itself — a
|
holds a random token; the database stores **SHA-256 of the token**, not the
|
||||||
stolen DB or backup archive does not alone grant login, but a browser that still
|
token itself — a stolen DB or backup archive does not alone grant login, but a
|
||||||
holds the cookie works after process restart, redeploy, or full backup restore.
|
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
|
- **Idle timeout** — sliding window, `PANEL_SESSION_IDLE_DAYS` (default 7); no
|
||||||
absolute cap (regular use keeps the session alive indefinitely).
|
absolute cap (regular use keeps the session alive indefinitely).
|
||||||
- **Renewal** — DB `last_seen` and cookie `Max-Age` update at most once per hour
|
- **Renewal** — the DB `expires_at` and the cookie `Max-Age` update at most once per
|
||||||
(`renewThreshold` in [internal/web/auth/session.go](../internal/web/auth/session.go)).
|
hour (`renewThreshold` in
|
||||||
- **Password change on `/settings`** — changing your own password deletes
|
[internal/web/auth/session.go](../internal/web/auth/session.go)).
|
||||||
every other session for that user; the current session stays active
|
- **Password change on Account** — deletes every other session of that user;
|
||||||
|
the current one stays active
|
||||||
([internal/store/sessions.go](../internal/store/sessions.go),
|
([internal/store/sessions.go](../internal/store/sessions.go),
|
||||||
[handlers_settings.go](../internal/web/handlers/handlers_settings.go)).
|
[handlers_account.go](../internal/web/handlers/handlers_account.go)).
|
||||||
A global administrator resetting another user's password on `/users` updates
|
- **A password set by a global user on the user form** deletes all of that
|
||||||
the hash but does not delete that user's existing sessions.
|
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
|
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
|
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,
|
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
|
`sasldb2` entry, Postfix map, OpenDKIM table) go through a service, which is
|
||||||
also where the rollback of a partial failure lives. Handlers may call
|
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
|
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
|
`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
|
about Postfix, OpenDKIM, DNS or the log file, which is what makes them
|
||||||
@@ -295,7 +461,7 @@ flowchart TB
|
|||||||
dmarcSvc["internal/dmarc"]
|
dmarcSvc["internal/dmarc"]
|
||||||
end
|
end
|
||||||
subgraph persistence ["Persistence"]
|
subgraph persistence ["Persistence"]
|
||||||
store["internal/store — SQLite, embedded migrations"]
|
store["internal/store — SQLite, embedded schema"]
|
||||||
end
|
end
|
||||||
subgraph adapters ["Adapters — the only infrastructure-aware code"]
|
subgraph adapters ["Adapters — the only infrastructure-aware code"]
|
||||||
postfix["internal/postfix"]
|
postfix["internal/postfix"]
|
||||||
@@ -344,7 +510,7 @@ single-connection trade-off that follows from it.
|
|||||||
|
|
||||||
| Path | Contents |
|
| 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 |
|
| `setup-token` | First-run setup token file |
|
||||||
| `opendkim/` | DKIM keys + tables |
|
| `opendkim/` | DKIM keys + tables |
|
||||||
| `sasl/sasldb2` | Application SASL credentials |
|
| `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
|
Not in `/data`: TLS certificates for the panel (reverse-proxy mount) — though
|
||||||
full backups also archive the operator's `./certs` PEM files when present.
|
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);
|
**Rotation:** send-log retention `SEND_LOG_RETENTION_DAYS` (default 90);
|
||||||
`mail.log` via logrotate (14 rotated files, check every 6h, rename +
|
`mail.log` via logrotate (14 rotated files, check every 6h, rename +
|
||||||
`postfix reload` in `postrotate` — see § Log tailer above).
|
`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`)
|
`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
|
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
|
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
|
inbound maps only — not DMARC maps. Stopped-container
|
||||||
`tar` of `./data` alone remains possible for state-only copies (see guide).
|
`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,
|
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
|
its counter and an end-of-stream flag (so truncation and reordering fail to
|
||||||
open). Full backup `.tar.gz` → `.spbk` (SelfPost backup), domain export
|
open). Full backup `.tar.gz` → `.spbk` (SelfPost backup), domain export
|
||||||
`.json` → `.spde` (SelfPost domain export); the plain forms remain the
|
`.json` → `.spde` (SelfPost domain export). In the panel the *Encrypt with a
|
||||||
default. Domain import detects the envelope by magic bytes; an encrypted full
|
password* box is ticked by default on both downloads and the plain form is
|
||||||
backup is converted back with `selfpost-backup -decrypt` before restore.
|
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.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
|
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 |
@@ -13,7 +13,7 @@
|
|||||||
<h1>The accepted direction, every screen</h1>
|
<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
|
<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
|
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-three screens and a components page live in
|
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
|
<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>
|
fonts and mark by relative path.</p>
|
||||||
|
|
||||||
@@ -21,7 +21,7 @@
|
|||||||
<div class="scroll">
|
<div class="scroll">
|
||||||
<table>
|
<table>
|
||||||
<tbody>
|
<tbody>
|
||||||
<tr><th>Signed out</th><td><a href="panel/login.html">Sign in</a> · <a href="panel/setup.html">Create administrator</a></td></tr>
|
<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>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> ·
|
<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-domain-settings.html">domain settings</a> · <a href="panel/out-app.html">application form</a> ·
|
||||||
|
|||||||
@@ -54,7 +54,7 @@
|
|||||||
<a class="navbar-item" href="account.html"><i class="ti ti-key"></i>Account</a>
|
<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>
|
<a class="navbar-item" href="help.html"><i class="ti ti-help"></i>Help</a>
|
||||||
<hr class="navbar-divider">
|
<hr class="navbar-divider">
|
||||||
<a class="navbar-item" href="login.html"><i class="ti ti-logout"></i>Sign out</a>
|
<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>
|
</div>
|
||||||
@@ -69,24 +69,26 @@
|
|||||||
<div class="sp-grow">
|
<div class="sp-grow">
|
||||||
<p class="sp-kicker">Signed in as admin · global</p>
|
<p class="sp-kicker">Signed in as admin · global</p>
|
||||||
<h1 class="title is-3">Account</h1>
|
<h1 class="title is-3">Account</h1>
|
||||||
<p class="sp-lead">Who you are to this panel, what it should tell you about, and where reports about your domains go. Applications keep their own logins and passwords and are not affected.</p>
|
<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>
|
</div>
|
||||||
|
|
||||||
<div class="columns">
|
<div class="columns">
|
||||||
<div class="column is-5">
|
<div class="column is-6">
|
||||||
<div class="box">
|
<div class="box">
|
||||||
<div class="sp-box-head"><span class="sp-no">01</span><h2>Profile</h2></div>
|
<div class="sp-box-head"><span class="sp-no">01</span><h2>Profile</h2></div>
|
||||||
<div class="sp-box-body">
|
<div class="sp-box-body">
|
||||||
<form class="sp-form" method="post" action="#">
|
<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="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>
|
<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">Where the panel writes to you: the notifications on the right and a password-reset link. It can also receive DMARC reports — your choice, under <a href="#dmarc">DMARC reports</a>.</p></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>
|
<button type="submit" class="button is-primary">Save profile</button>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="column is-6">
|
||||||
<div class="box">
|
<div class="box">
|
||||||
<div class="sp-box-head"><span class="sp-no">02</span><h2>Password</h2></div>
|
<div class="sp-box-head"><span class="sp-no">02</span><h2>Password</h2></div>
|
||||||
<div class="sp-box-body">
|
<div class="sp-box-body">
|
||||||
@@ -102,55 +104,6 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="column is-7">
|
|
||||||
<div class="box">
|
|
||||||
<div class="sp-box-head"><span class="sp-no">03</span><h2>Notifications</h2><div class="sp-end">Sent to mix@example.org</div></div>
|
|
||||||
<div class="sp-box-body">
|
|
||||||
<form class="sp-form" method="post" action="#">
|
|
||||||
<p class="sp-kicker mb-2">Your domains</p>
|
|
||||||
<div class="field"><label class="checkbox"><input type="checkbox" name="ev" checked> A DNS record stops matching — DKIM, SPF, DMARC or an inbound MX</label></div>
|
|
||||||
<div class="field"><label class="checkbox"><input type="checkbox" name="ev" checked> A domain or an application reaches its rate limit and mail is being refused</label></div>
|
|
||||||
<div class="field"><label class="checkbox"><input type="checkbox" name="ev" checked> An inbound domain's upstream cannot be reached and mail is piling up</label></div>
|
|
||||||
<div class="field"><label class="checkbox"><input type="checkbox" name="ev"> DMARC reports start showing failures for a domain</label></div>
|
|
||||||
<div class="field"><label class="checkbox"><input type="checkbox" name="ev"> Mail is held in quarantine</label></div>
|
|
||||||
<p class="sp-kicker mt-4 mb-2">The server · global role</p>
|
|
||||||
<div class="field"><label class="checkbox"><input type="checkbox" name="ev" checked> The TLS certificate is within 14 days of expiry</label></div>
|
|
||||||
<div class="field"><label class="checkbox"><input type="checkbox" name="ev" checked> A process or a milter socket stops answering</label></div>
|
|
||||||
<div class="field"><label class="checkbox"><input type="checkbox" name="ev"> Messages sit in a queue for more than a day</label></div>
|
|
||||||
<p class="help mb-3">One message when a problem starts and one when it clears — never a stream. A domain administrator is told only about the domains assigned to them.</p>
|
|
||||||
<button type="submit" class="button is-primary">Save notifications</button>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="box" id="dmarc">
|
|
||||||
<div class="sp-box-head"><span class="sp-no">04</span><h2>DMARC reports</h2><div class="sp-end"><span class="tag is-success is-light">authorized</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-record">
|
|
||||||
<form class="sp-form" method="post" action="#">
|
|
||||||
<div class="field"><label class="label" for="rua">My default report address</label>
|
|
||||||
<div class="control"><div class="select is-fullwidth"><select id="rua" name="dmarc_default">
|
|
||||||
<option>SelfPost hosted — read them under DMARC reports</option>
|
|
||||||
<option selected>My account e-mail — mix@example.org</option>
|
|
||||||
<option>Another address…</option>
|
|
||||||
<option>No reports</option>
|
|
||||||
</select></div></div>
|
|
||||||
<p class="help">Used by every outbound domain whose report address is set to <em>admin's default</em> — 2 of your 3 now. Changing it changes the DMARC record those domains must publish. The hosted address is parsed by SelfPost; any other just receives the raw XML.</p></div>
|
|
||||||
<button type="submit" class="button is-primary">Save report address</button>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
<div class="sp-record">
|
|
||||||
<div class="sp-record-head"><h3>Report authorization</h3><span class="tag is-success is-light">published</span><small>Needed because the address is on another domain than the senders</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="*._report._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"></div><div class="control"><button type="button" class="button"><i class="ti ti-copy"></i>Copy</button></div></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>
|
<footer class="sp-foot">SelfPost 1.8.0 · © 2026 Mix · <a href="#">License (AGPL-3.0)</a> · <a href="#">Source</a> · No warranty</footer>
|
||||||
|
|||||||
@@ -54,7 +54,7 @@
|
|||||||
<a class="navbar-item" href="account.html"><i class="ti ti-key"></i>Account</a>
|
<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>
|
<a class="navbar-item" href="help.html"><i class="ti ti-help"></i>Help</a>
|
||||||
<hr class="navbar-divider">
|
<hr class="navbar-divider">
|
||||||
<a class="navbar-item" href="login.html"><i class="ti ti-logout"></i>Sign out</a>
|
<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>
|
</div>
|
||||||
@@ -76,7 +76,7 @@
|
|||||||
<div class="columns">
|
<div class="columns">
|
||||||
<div class="column is-6">
|
<div class="column is-6">
|
||||||
<div class="box">
|
<div class="box">
|
||||||
<div class="sp-box-head"><span class="sp-no">01</span><h2>Full backup</h2></div>
|
<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">
|
<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>
|
<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="#">
|
<form class="sp-form" method="post" action="#">
|
||||||
@@ -94,9 +94,9 @@
|
|||||||
|
|
||||||
<div class="column is-6">
|
<div class="column is-6">
|
||||||
<div class="box" id="import">
|
<div class="box" id="import">
|
||||||
<div class="sp-box-head"><span class="sp-no">02</span><h2>Import a domain</h2></div>
|
<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">
|
<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#export">domain's settings</a>.</p>
|
<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">
|
<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="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="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>
|
||||||
|
|||||||
@@ -81,7 +81,9 @@ def navbar(section, page):
|
|||||||
' <a class="navbar-item" href="account.html"><i class="ti ti-key"></i>Account</a>',
|
' <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>',
|
' <a class="navbar-item" href="help.html"><i class="ti ti-help"></i>Help</a>',
|
||||||
' <hr class="navbar-divider">',
|
' <hr class="navbar-divider">',
|
||||||
' <a class="navbar-item" href="login.html"><i class="ti ti-logout"></i>Sign out</a>',
|
# 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>', ' </div>', ' </div>', ' </div>', ' </div>', '</nav>',
|
||||||
'<div class="sp-perf" aria-hidden="true"></div>']
|
'<div class="sp-perf" aria-hidden="true"></div>']
|
||||||
return "\n".join(out)
|
return "\n".join(out)
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ box button buttons checkbox column columns control field file file-cta file-inpu
|
|||||||
file-name has-addons has-icons-left has-name has-text-danger help icon input label menu menu-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
|
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-3 is-4 is-5 is-6 is-7 is-9 is-active is-danger is-expanded is-fullwidth is-gapless is-half
|
||||||
is-hoverable is-left is-light is-primary is-small is-success is-text is-warning
|
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
|
mb-1 mb-2 mb-3 mb-4 ml-2 mt-4 mt-5
|
||||||
""".split())
|
""".split())
|
||||||
# Classes the shell (build.py, later layout.html) owns; screens never write them.
|
# Classes the shell (build.py, later layout.html) owns; screens never write them.
|
||||||
|
|||||||
@@ -54,7 +54,7 @@
|
|||||||
<a class="navbar-item" href="account.html"><i class="ti ti-key"></i>Account</a>
|
<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>
|
<a class="navbar-item" href="help.html"><i class="ti ti-help"></i>Help</a>
|
||||||
<hr class="navbar-divider">
|
<hr class="navbar-divider">
|
||||||
<a class="navbar-item" href="login.html"><i class="ti ti-logout"></i>Sign out</a>
|
<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>
|
</div>
|
||||||
@@ -172,7 +172,7 @@
|
|||||||
<div class="table-container"><table class="table is-fullwidth is-hoverable">
|
<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>
|
<thead><tr><th>Time, UTC</th><th>Name</th><th>sp-clip</th><th>Status</th><th></th></tr></thead>
|
||||||
<tbody>
|
<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><a class="has-text-danger" href="#">Delete</a></td></tr>
|
<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>
|
</tbody>
|
||||||
</table></div>
|
</table></div>
|
||||||
<div class="sp-box-foot"><span>Page 1 of 9</span><a href="#">Older →</a></div>
|
<div class="sp-box-foot"><span>Page 1 of 9</span><a href="#">Older →</a></div>
|
||||||
|
|||||||
@@ -54,7 +54,7 @@
|
|||||||
<a class="navbar-item" href="account.html"><i class="ti ti-key"></i>Account</a>
|
<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>
|
<a class="navbar-item" href="help.html"><i class="ti ti-help"></i>Help</a>
|
||||||
<hr class="navbar-divider">
|
<hr class="navbar-divider">
|
||||||
<a class="navbar-item" href="login.html"><i class="ti ti-logout"></i>Sign out</a>
|
<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>
|
</div>
|
||||||
@@ -78,7 +78,7 @@
|
|||||||
<div class="columns">
|
<div class="columns">
|
||||||
<div class="column is-5">
|
<div class="column is-5">
|
||||||
<div class="box">
|
<div class="box">
|
||||||
<div class="sp-box-head"><span class="sp-no">01</span><h2>Sources · 7 days</h2></div>
|
<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">
|
<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>
|
<thead><tr><th>Source</th><th>Pass</th><th>Fail</th><th>Disposition</th></tr></thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
|
|||||||
@@ -54,7 +54,7 @@
|
|||||||
<a class="navbar-item" href="account.html"><i class="ti ti-key"></i>Account</a>
|
<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>
|
<a class="navbar-item" href="help.html"><i class="ti ti-help"></i>Help</a>
|
||||||
<hr class="navbar-divider">
|
<hr class="navbar-divider">
|
||||||
<a class="navbar-item" href="login.html"><i class="ti ti-logout"></i>Sign out</a>
|
<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>
|
</div>
|
||||||
@@ -76,7 +76,7 @@
|
|||||||
<div class="columns">
|
<div class="columns">
|
||||||
<div class="column is-6">
|
<div class="column is-6">
|
||||||
<div class="box">
|
<div class="box">
|
||||||
<div class="sp-box-head"><span class="sp-no">01</span><h2>Report</h2></div>
|
<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">
|
<div class="sp-box-body">
|
||||||
<dl class="sp-facts">
|
<dl class="sp-facts">
|
||||||
<div><dt>Reporter</dt><dd>Outlook.com</dd></div>
|
<div><dt>Reporter</dt><dd>Outlook.com</dd></div>
|
||||||
|
|||||||
@@ -54,7 +54,7 @@
|
|||||||
<a class="navbar-item" href="account.html"><i class="ti ti-key"></i>Account</a>
|
<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>
|
<a class="navbar-item" href="help.html"><i class="ti ti-help"></i>Help</a>
|
||||||
<hr class="navbar-divider">
|
<hr class="navbar-divider">
|
||||||
<a class="navbar-item" href="login.html"><i class="ti ti-logout"></i>Sign out</a>
|
<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>
|
</div>
|
||||||
@@ -74,12 +74,12 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="box">
|
<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></div>
|
<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">
|
<div class="sp-box-body">
|
||||||
<dl class="sp-facts">
|
<dl class="sp-facts">
|
||||||
<div><dt>Last report</dt><dd>2026-09-21 06:12 UTC</dd></div>
|
<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>This week</dt><dd>14 kept · 0 parse failures</dd></div>
|
||||||
<div><dt>Hosted address</dt><dd class="sp-mono">dmarc@mail.example.org</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>
|
<div><dt>Retention</dt><dd>500 reports or 90 days</dd></div>
|
||||||
</dl>
|
</dl>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -54,7 +54,7 @@
|
|||||||
<a class="navbar-item" href="account.html"><i class="ti ti-key"></i>Account</a>
|
<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>
|
<a class="navbar-item" href="help.html"><i class="ti ti-help"></i>Help</a>
|
||||||
<hr class="navbar-divider">
|
<hr class="navbar-divider">
|
||||||
<a class="navbar-item" href="login.html"><i class="ti ti-logout"></i>Sign out</a>
|
<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>
|
</div>
|
||||||
@@ -77,7 +77,7 @@
|
|||||||
<div class="columns">
|
<div class="columns">
|
||||||
<div class="column is-6">
|
<div class="column is-6">
|
||||||
<div class="box" id="machine">
|
<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></div>
|
<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">
|
<div class="table-container"><table class="table is-fullwidth">
|
||||||
<thead><tr><th>Resource</th><th>Usage</th><th>Detail</th></tr></thead>
|
<thead><tr><th>Resource</th><th>Usage</th><th>Detail</th></tr></thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
@@ -108,7 +108,7 @@
|
|||||||
<div class="columns">
|
<div class="columns">
|
||||||
<div class="column is-4">
|
<div class="column is-4">
|
||||||
<div class="box" id="certificate">
|
<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></div>
|
<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">
|
<div class="sp-box-body">
|
||||||
<dl class="sp-facts">
|
<dl class="sp-facts">
|
||||||
<div><dt>Expires</dt><dd class="sp-mono">2026-10-03 09:14 UTC</dd></div>
|
<div><dt>Expires</dt><dd class="sp-mono">2026-10-03 09:14 UTC</dd></div>
|
||||||
@@ -134,7 +134,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="column is-4">
|
<div class="column is-4">
|
||||||
<div class="box" id="hostname">
|
<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></div>
|
<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">
|
<div class="sp-box-body">
|
||||||
<dl class="sp-facts">
|
<dl class="sp-facts">
|
||||||
<div><dt>Hostname</dt><dd class="sp-mono">mail.example.org</dd></div>
|
<div><dt>Hostname</dt><dd class="sp-mono">mail.example.org</dd></div>
|
||||||
|
|||||||
@@ -54,7 +54,7 @@
|
|||||||
<a class="navbar-item" href="account.html"><i class="ti ti-key"></i>Account</a>
|
<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>
|
<a class="navbar-item" href="help.html"><i class="ti ti-help"></i>Help</a>
|
||||||
<hr class="navbar-divider">
|
<hr class="navbar-divider">
|
||||||
<a class="navbar-item" href="login.html"><i class="ti ti-logout"></i>Sign out</a>
|
<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>
|
</div>
|
||||||
@@ -69,7 +69,7 @@
|
|||||||
<div class="sp-grow">
|
<div class="sp-grow">
|
||||||
<p class="sp-kicker">Reference</p>
|
<p class="sp-kicker">Reference</p>
|
||||||
<h1 class="title is-3">Help</h1>
|
<h1 class="title is-3">Help</h1>
|
||||||
<p class="sp-lead">What each screen checks and what to do when it is not green. Every numbered section on a page links to its topic here.</p>
|
<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>
|
</div>
|
||||||
|
|
||||||
@@ -87,27 +87,61 @@
|
|||||||
<li><a href="#dns"><i class="ti ti-world-check"></i>DNS records</a></li>
|
<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="#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="#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="#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>
|
<li><a href="#dmarc"><i class="ti ti-report-analytics"></i>DMARC reports</a></li>
|
||||||
</ul>
|
</ul>
|
||||||
<p class="menu-label">Inbound</p>
|
<p class="menu-label">Inbound</p>
|
||||||
<ul class="menu-list">
|
<ul class="menu-list">
|
||||||
<li><a href="#inbound"><i class="ti ti-inbox"></i>Backup-MX and forwarding</a></li>
|
<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>
|
</ul>
|
||||||
</aside>
|
</aside>
|
||||||
</div>
|
</div>
|
||||||
<div class="column is-9">
|
<div class="column is-9">
|
||||||
<div class="box">
|
<div class="box">
|
||||||
<div class="sp-box-head"><span class="sp-no">01</span><h2>Overview</h2></div>
|
<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, 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.</p></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. It warns 14 days before expiry and fails once expired — clients that verify certificates then refuse to connect.</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 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 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>
|
||||||
<div class="box">
|
<div class="box">
|
||||||
<div class="sp-box-head"><span class="sp-no">02</span><h2>Outbound</h2></div>
|
<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.</p></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. It may send as any address of its domain or only as listed 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="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, manual or computed from 30-day statistics, and can never exceed level 1.</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>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -54,7 +54,7 @@
|
|||||||
<a class="navbar-item" href="account.html"><i class="ti ti-key"></i>Account</a>
|
<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>
|
<a class="navbar-item" href="help.html"><i class="ti ti-help"></i>Help</a>
|
||||||
<hr class="navbar-divider">
|
<hr class="navbar-divider">
|
||||||
<a class="navbar-item" href="login.html"><i class="ti ti-logout"></i>Sign out</a>
|
<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>
|
</div>
|
||||||
|
|||||||
@@ -54,7 +54,7 @@
|
|||||||
<a class="navbar-item" href="account.html"><i class="ti ti-key"></i>Account</a>
|
<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>
|
<a class="navbar-item" href="help.html"><i class="ti ti-help"></i>Help</a>
|
||||||
<hr class="navbar-divider">
|
<hr class="navbar-divider">
|
||||||
<a class="navbar-item" href="login.html"><i class="ti ti-logout"></i>Sign out</a>
|
<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>
|
</div>
|
||||||
@@ -116,7 +116,7 @@
|
|||||||
<div class="columns">
|
<div class="columns">
|
||||||
<div class="column is-6">
|
<div class="column is-6">
|
||||||
<div class="box" id="upstream">
|
<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></div>
|
<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">
|
<div class="sp-box-body">
|
||||||
<form class="sp-form" method="post" action="#">
|
<form class="sp-form" method="post" action="#">
|
||||||
<div class="sp-row">
|
<div class="sp-row">
|
||||||
@@ -132,7 +132,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="column is-6">
|
<div class="column is-6">
|
||||||
<div class="box" id="recipients">
|
<div class="box" id="recipients">
|
||||||
<div class="sp-box-head"><span class="sp-no">03</span><h2>Valid recipients</h2></div>
|
<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">
|
<div class="sp-box-body">
|
||||||
<form class="sp-form" method="post" action="#">
|
<form class="sp-form" method="post" action="#">
|
||||||
<div class="field"><label class="label" for="rmode">Who to accept</label>
|
<div class="field"><label class="label" for="rmode">Who to accept</label>
|
||||||
@@ -151,7 +151,7 @@ security@lists.example.org</textarea></div>
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="box" id="filter">
|
<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></div>
|
<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">
|
<div class="sp-box-body">
|
||||||
<dl class="sp-facts">
|
<dl class="sp-facts">
|
||||||
<div><dt>Engine</dt><dd>rspamd, for every inbound domain of this server</dd></div>
|
<div><dt>Engine</dt><dd>rspamd, for every inbound domain of this server</dd></div>
|
||||||
|
|||||||
@@ -54,7 +54,7 @@
|
|||||||
<a class="navbar-item" href="account.html"><i class="ti ti-key"></i>Account</a>
|
<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>
|
<a class="navbar-item" href="help.html"><i class="ti ti-help"></i>Help</a>
|
||||||
<hr class="navbar-divider">
|
<hr class="navbar-divider">
|
||||||
<a class="navbar-item" href="login.html"><i class="ti ti-logout"></i>Sign out</a>
|
<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>
|
</div>
|
||||||
@@ -87,7 +87,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="box">
|
<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></div>
|
<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">
|
<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>
|
<thead><tr><th>Domain</th><th>DNS</th><th>Upstream</th><th>Recipients</th><th>TLS to upstream</th><th></th></tr></thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
|
|||||||
@@ -54,7 +54,7 @@
|
|||||||
<a class="navbar-item" href="account.html"><i class="ti ti-key"></i>Account</a>
|
<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>
|
<a class="navbar-item" href="help.html"><i class="ti ti-help"></i>Help</a>
|
||||||
<hr class="navbar-divider">
|
<hr class="navbar-divider">
|
||||||
<a class="navbar-item" href="login.html"><i class="ti ti-logout"></i>Sign out</a>
|
<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>
|
</div>
|
||||||
|
|||||||
@@ -54,7 +54,7 @@
|
|||||||
<a class="navbar-item" href="account.html"><i class="ti ti-key"></i>Account</a>
|
<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>
|
<a class="navbar-item" href="help.html"><i class="ti ti-help"></i>Help</a>
|
||||||
<hr class="navbar-divider">
|
<hr class="navbar-divider">
|
||||||
<a class="navbar-item" href="login.html"><i class="ti ti-logout"></i>Sign out</a>
|
<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>
|
</div>
|
||||||
|
|||||||
@@ -54,7 +54,7 @@
|
|||||||
<a class="navbar-item" href="account.html"><i class="ti ti-key"></i>Account</a>
|
<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>
|
<a class="navbar-item" href="help.html"><i class="ti ti-help"></i>Help</a>
|
||||||
<hr class="navbar-divider">
|
<hr class="navbar-divider">
|
||||||
<a class="navbar-item" href="login.html"><i class="ti ti-logout"></i>Sign out</a>
|
<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>
|
</div>
|
||||||
|
|||||||
@@ -54,7 +54,7 @@
|
|||||||
<a class="navbar-item" href="account.html"><i class="ti ti-key"></i>Account</a>
|
<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>
|
<a class="navbar-item" href="help.html"><i class="ti ti-help"></i>Help</a>
|
||||||
<hr class="navbar-divider">
|
<hr class="navbar-divider">
|
||||||
<a class="navbar-item" href="login.html"><i class="ti ti-logout"></i>Sign out</a>
|
<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>
|
</div>
|
||||||
|
|||||||
@@ -54,7 +54,7 @@
|
|||||||
<a class="navbar-item" href="account.html"><i class="ti ti-key"></i>Account</a>
|
<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>
|
<a class="navbar-item" href="help.html"><i class="ti ti-help"></i>Help</a>
|
||||||
<hr class="navbar-divider">
|
<hr class="navbar-divider">
|
||||||
<a class="navbar-item" href="login.html"><i class="ti ti-logout"></i>Sign out</a>
|
<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>
|
</div>
|
||||||
|
|||||||
@@ -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>
|
||||||
@@ -54,7 +54,7 @@
|
|||||||
<a class="navbar-item" href="account.html"><i class="ti ti-key"></i>Account</a>
|
<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>
|
<a class="navbar-item" href="help.html"><i class="ti ti-help"></i>Help</a>
|
||||||
<hr class="navbar-divider">
|
<hr class="navbar-divider">
|
||||||
<a class="navbar-item" href="login.html"><i class="ti ti-logout"></i>Sign out</a>
|
<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>
|
</div>
|
||||||
|
|||||||
@@ -54,7 +54,7 @@
|
|||||||
<a class="navbar-item" href="account.html"><i class="ti ti-key"></i>Account</a>
|
<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>
|
<a class="navbar-item" href="help.html"><i class="ti ti-help"></i>Help</a>
|
||||||
<hr class="navbar-divider">
|
<hr class="navbar-divider">
|
||||||
<a class="navbar-item" href="login.html"><i class="ti ti-logout"></i>Sign out</a>
|
<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>
|
</div>
|
||||||
@@ -97,7 +97,7 @@
|
|||||||
|
|
||||||
<div class="column is-4">
|
<div class="column is-4">
|
||||||
<div class="box">
|
<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></div>
|
<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="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="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="field"><label class="label" for="ips">Allowed client IPs</label>
|
||||||
@@ -109,7 +109,7 @@
|
|||||||
|
|
||||||
<div class="column is-4">
|
<div class="column is-4">
|
||||||
<div class="box">
|
<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></div>
|
<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="sp-box-body">
|
||||||
<div class="field"><label class="label" for="rmode">Limit mode</label>
|
<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="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>
|
||||||
|
|||||||
@@ -54,7 +54,7 @@
|
|||||||
<a class="navbar-item" href="account.html"><i class="ti ti-key"></i>Account</a>
|
<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>
|
<a class="navbar-item" href="help.html"><i class="ti ti-help"></i>Help</a>
|
||||||
<hr class="navbar-divider">
|
<hr class="navbar-divider">
|
||||||
<a class="navbar-item" href="login.html"><i class="ti ti-logout"></i>Sign out</a>
|
<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>
|
</div>
|
||||||
@@ -97,7 +97,7 @@
|
|||||||
<div class="box">
|
<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-head"><span class="sp-no"><i class="ti ti-file-export"></i></span><h2>Moving it instead?</h2></div>
|
||||||
<div class="sp-box-body">
|
<div class="sp-box-body">
|
||||||
<p><a href="out-domain-settings.html#export">Export the domain</a> first. The file carries the key and the application passwords, so the new instance needs no DNS change.</p>
|
<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>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -54,7 +54,7 @@
|
|||||||
<a class="navbar-item" href="account.html"><i class="ti ti-key"></i>Account</a>
|
<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>
|
<a class="navbar-item" href="help.html"><i class="ti ti-help"></i>Help</a>
|
||||||
<hr class="navbar-divider">
|
<hr class="navbar-divider">
|
||||||
<a class="navbar-item" href="login.html"><i class="ti ti-logout"></i>Sign out</a>
|
<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>
|
</div>
|
||||||
@@ -76,25 +76,25 @@
|
|||||||
<div class="columns">
|
<div class="columns">
|
||||||
<div class="column is-6">
|
<div class="column is-6">
|
||||||
<div class="box" id="reports">
|
<div class="box" id="reports">
|
||||||
<div class="sp-box-head"><span class="sp-no">01</span><h2>DMARC report address</h2></div>
|
<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">
|
<div class="sp-box-body">
|
||||||
<form class="sp-form" method="post" action="#">
|
<form class="sp-form" method="post" action="#">
|
||||||
<div class="field"><label class="label" for="rua">Aggregate reports (rua=)</label>
|
<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">
|
<div class="control"><div class="select is-fullwidth"><select id="rua" name="dmarc_rua_mode">
|
||||||
<option selected>admin's default — mix@example.org</option>
|
<option>No reports</option>
|
||||||
<option>SelfPost hosted (example.org@dmarc.mail.example.org)</option>
|
<option>SelfPost hosted — example.org@dmarc.mail.example.org</option>
|
||||||
<option>My default instead — (you are admin)</option>
|
<option selected>A specific address</option>
|
||||||
<option>No aggregate reports</option>
|
|
||||||
<option>Custom address</option>
|
|
||||||
</select></div></div>
|
</select></div></div>
|
||||||
<p class="help">A default belongs to a user and is set under their <a href="account.html#dmarc">Account</a>; a domain follows one named user, so two people sharing a domain never pull it two ways. Changing this changes the DMARC record to publish.</p></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>
|
<button type="submit" class="button is-primary">Save report address</button>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="box" id="export">
|
<div class="box" id="export">
|
||||||
<div class="sp-box-head"><span class="sp-no">03</span><h2>Export domain</h2></div>
|
<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">
|
<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>
|
<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="#">
|
<form class="sp-form" method="post" action="#">
|
||||||
|
|||||||
@@ -54,7 +54,7 @@
|
|||||||
<a class="navbar-item" href="account.html"><i class="ti ti-key"></i>Account</a>
|
<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>
|
<a class="navbar-item" href="help.html"><i class="ti ti-help"></i>Help</a>
|
||||||
<hr class="navbar-divider">
|
<hr class="navbar-divider">
|
||||||
<a class="navbar-item" href="login.html"><i class="ti ti-logout"></i>Sign out</a>
|
<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>
|
</div>
|
||||||
@@ -94,9 +94,9 @@
|
|||||||
</ul>
|
</ul>
|
||||||
<p class="menu-label">Rarely changed</p>
|
<p class="menu-label">Rarely changed</p>
|
||||||
<ul class="menu-list">
|
<ul class="menu-list">
|
||||||
<li><a href="out-domain-settings.html#reports"><i class="ti ti-mail-forward"></i>Report address</a></li>
|
<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#limit"><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-gauge"></i>Rate limit <span class="tag is-success is-light">auto</span></a></li>
|
||||||
<li><a href="out-domain-settings.html#export"><i class="ti ti-file-export"></i>Export</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>
|
<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>
|
</ul>
|
||||||
</aside>
|
</aside>
|
||||||
@@ -132,14 +132,14 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="sp-record">
|
<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#reports">admin's default</a></small></div>
|
<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="sp-row">
|
||||||
<div class="field"><label class="label">Host / name</label>
|
<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 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 class="field"><label class="label">Type</label><div class="control"><input class="input" readonly value="TXT"></div></div>
|
||||||
</div>
|
</div>
|
||||||
<div class="field"><label class="label">Value to publish</label>
|
<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"><i class="ti ti-copy"></i>Copy</button></div></div>
|
<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>
|
<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>
|
</div>
|
||||||
@@ -149,10 +149,10 @@
|
|||||||
<div class="table-container"><table class="table is-fullwidth is-hoverable">
|
<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>
|
<thead><tr><th>Login</th><th>May send as</th><th>30 days</th><th>Limits</th><th></th></tr></thead>
|
||||||
<tbody>
|
<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>
|
<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><a href="out-app-created.html">New password</a><a class="has-text-danger" href="#">Delete</a></td></tr>
|
<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"><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>
|
<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><a href="out-app-created.html">New password</a><a class="has-text-danger" href="#">Delete</a></td></tr>
|
<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>
|
</tbody>
|
||||||
</table></div>
|
</table></div>
|
||||||
<div class="sp-box-foot">New password and Delete ask for confirmation; the old password stops working at once.</div>
|
<div class="sp-box-foot">New password and Delete ask for confirmation; the old password stops working at once.</div>
|
||||||
|
|||||||
@@ -54,7 +54,7 @@
|
|||||||
<a class="navbar-item" href="account.html"><i class="ti ti-key"></i>Account</a>
|
<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>
|
<a class="navbar-item" href="help.html"><i class="ti ti-help"></i>Help</a>
|
||||||
<hr class="navbar-divider">
|
<hr class="navbar-divider">
|
||||||
<a class="navbar-item" href="login.html"><i class="ti ti-logout"></i>Sign out</a>
|
<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>
|
</div>
|
||||||
|
|||||||
@@ -54,7 +54,7 @@
|
|||||||
<a class="navbar-item" href="account.html"><i class="ti ti-key"></i>Account</a>
|
<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>
|
<a class="navbar-item" href="help.html"><i class="ti ti-help"></i>Help</a>
|
||||||
<hr class="navbar-divider">
|
<hr class="navbar-divider">
|
||||||
<a class="navbar-item" href="login.html"><i class="ti ti-logout"></i>Sign out</a>
|
<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>
|
</div>
|
||||||
@@ -82,6 +82,7 @@
|
|||||||
<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>
|
<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>
|
<button type="submit" class="button"><i class="ti ti-filter"></i>Filter</button>
|
||||||
</form>
|
</form>
|
||||||
|
<a class="sp-help" href="help.html#log" title="What the statuses mean"><i class="ti ti-help-circle"></i></a>
|
||||||
</div>
|
</div>
|
||||||
<div class="table-container"><table class="table is-fullwidth is-hoverable">
|
<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>
|
<thead><tr><th>Time, UTC</th><th>From</th><th>To</th><th>Subject</th><th>Status</th><th></th></tr></thead>
|
||||||
|
|||||||
@@ -54,7 +54,7 @@
|
|||||||
<a class="navbar-item" href="account.html"><i class="ti ti-key"></i>Account</a>
|
<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>
|
<a class="navbar-item" href="help.html"><i class="ti ti-help"></i>Help</a>
|
||||||
<hr class="navbar-divider">
|
<hr class="navbar-divider">
|
||||||
<a class="navbar-item" href="login.html"><i class="ti ti-logout"></i>Sign out</a>
|
<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>
|
</div>
|
||||||
@@ -91,7 +91,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="column is-5">
|
<div class="column is-5">
|
||||||
<div class="box">
|
<div class="box">
|
||||||
<div class="sp-box-head"><span class="sp-no">02</span><h2>History</h2></div>
|
<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">
|
<div class="sp-box-body">
|
||||||
<ol class="sp-timeline">
|
<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><time>13:58:07 UTC</time><strong>Accepted</strong> from <span class="sp-mono">alerts</span> at 198.51.100.7</li>
|
||||||
|
|||||||
@@ -54,7 +54,7 @@
|
|||||||
<a class="navbar-item" href="account.html"><i class="ti ti-key"></i>Account</a>
|
<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>
|
<a class="navbar-item" href="help.html"><i class="ti ti-help"></i>Help</a>
|
||||||
<hr class="navbar-divider">
|
<hr class="navbar-divider">
|
||||||
<a class="navbar-item" href="login.html"><i class="ti ti-logout"></i>Sign out</a>
|
<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>
|
</div>
|
||||||
@@ -74,7 +74,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="box">
|
<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></div>
|
<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">
|
<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>
|
<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>
|
<tbody>
|
||||||
|
|||||||
@@ -4,15 +4,13 @@
|
|||||||
{
|
{
|
||||||
"columns": [
|
"columns": [
|
||||||
{
|
{
|
||||||
"is-5": [
|
"is-6": [
|
||||||
"box[Profile]:body",
|
"box[Profile]:body"
|
||||||
"box[Password]:body"
|
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"is-7": [
|
"is-6": [
|
||||||
"box[Notifications]:body",
|
"box[Password]:body"
|
||||||
"box[DMARC reports]:record×2"
|
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
@@ -124,7 +122,10 @@
|
|||||||
{
|
{
|
||||||
"is-9": [
|
"is-9": [
|
||||||
"box[Overview]:help×3",
|
"box[Overview]:help×3",
|
||||||
"box[Outbound]:help×3"
|
"box[Outbound]:help×6",
|
||||||
|
"box[Inbound]:help×4",
|
||||||
|
"box[Server]:help×3",
|
||||||
|
"box[Account]:help"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
@@ -405,8 +406,7 @@
|
|||||||
"columns": [
|
"columns": [
|
||||||
{
|
{
|
||||||
"is-6": [
|
"is-6": [
|
||||||
"box[Log retention]:body",
|
"box[Log retention]:body"
|
||||||
"box[Notifications]:body"
|
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -54,7 +54,7 @@
|
|||||||
<a class="navbar-item" href="account.html"><i class="ti ti-key"></i>Account</a>
|
<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>
|
<a class="navbar-item" href="help.html"><i class="ti ti-help"></i>Help</a>
|
||||||
<hr class="navbar-divider">
|
<hr class="navbar-divider">
|
||||||
<a class="navbar-item" href="login.html"><i class="ti ti-logout"></i>Sign out</a>
|
<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>
|
</div>
|
||||||
@@ -78,12 +78,12 @@
|
|||||||
<div class="box">
|
<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-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">
|
<div class="sp-health">
|
||||||
<a href="health.html#machine"><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-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#processes"><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"><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#certificate" 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="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="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#sockets"><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-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#hostname"><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>
|
<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>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -54,7 +54,7 @@
|
|||||||
<a class="navbar-item" href="account.html"><i class="ti ti-key"></i>Account</a>
|
<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>
|
<a class="navbar-item" href="help.html"><i class="ti ti-help"></i>Help</a>
|
||||||
<hr class="navbar-divider">
|
<hr class="navbar-divider">
|
||||||
<a class="navbar-item" href="login.html"><i class="ti ti-logout"></i>Sign out</a>
|
<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>
|
</div>
|
||||||
|
|||||||
@@ -54,7 +54,7 @@
|
|||||||
<a class="navbar-item" href="account.html"><i class="ti ti-key"></i>Account</a>
|
<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>
|
<a class="navbar-item" href="help.html"><i class="ti ti-help"></i>Help</a>
|
||||||
<hr class="navbar-divider">
|
<hr class="navbar-divider">
|
||||||
<a class="navbar-item" href="login.html"><i class="ti ti-logout"></i>Sign out</a>
|
<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>
|
</div>
|
||||||
@@ -69,7 +69,7 @@
|
|||||||
<div class="sp-grow">
|
<div class="sp-grow">
|
||||||
<p class="sp-kicker">Server</p>
|
<p class="sp-kicker">Server</p>
|
||||||
<h1 class="title is-3">Settings</h1>
|
<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, notifications and DMARC report address are under <a href="account.html">Account</a>.</p>
|
<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>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -77,30 +77,22 @@
|
|||||||
<div class="columns">
|
<div class="columns">
|
||||||
<div class="column is-6">
|
<div class="column is-6">
|
||||||
<div class="box">
|
<div class="box">
|
||||||
<div class="sp-box-head"><span class="sp-no">01</span><h2>Log retention</h2></div>
|
<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="sp-box-body">
|
||||||
<div class="field"><label class="label" for="ret">Keep outbound log rows, days</label>
|
<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>
|
<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>
|
<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>
|
||||||
|
|
||||||
<div class="box">
|
|
||||||
<div class="sp-box-head"><span class="sp-no">02</span><h2>Notifications</h2></div>
|
|
||||||
<div class="sp-box-body">
|
|
||||||
<div class="field"><label class="checkbox"><input type="checkbox" name="notify" checked> Send notification e-mails</label></div>
|
|
||||||
<p class="help">Sent through this relay from <span class="sp-mono">postmaster@mail.example.org</span>. Each user picks the events under <a href="account.html">Account</a>; a user without an e-mail gets none.</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="column is-6">
|
<div class="column is-6">
|
||||||
<div class="box" id="rate-limits">
|
<div class="box" id="rate-limits">
|
||||||
<div class="sp-box-head"><span class="sp-no">03</span><h2>Sending rate limits</h2><div class="sp-end">Read-only</div></div>
|
<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">
|
<div class="sp-box-body">
|
||||||
<dl class="sp-facts">
|
<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 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#limit">domain</a> and <a href="out-app.html">application</a>.</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>
|
</dl>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -3,24 +3,26 @@
|
|||||||
<div class="sp-grow">
|
<div class="sp-grow">
|
||||||
<p class="sp-kicker">Signed in as admin · global</p>
|
<p class="sp-kicker">Signed in as admin · global</p>
|
||||||
<h1 class="title is-3">Account</h1>
|
<h1 class="title is-3">Account</h1>
|
||||||
<p class="sp-lead">Who you are to this panel, what it should tell you about, and where reports about your domains go. Applications keep their own logins and passwords and are not affected.</p>
|
<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>
|
</div>
|
||||||
|
|
||||||
<div class="columns">
|
<div class="columns">
|
||||||
<div class="column is-5">
|
<div class="column is-6">
|
||||||
<div class="box">
|
<div class="box">
|
||||||
<div class="sp-box-head"><span class="sp-no">01</span><h2>Profile</h2></div>
|
<div class="sp-box-head"><span class="sp-no">01</span><h2>Profile</h2></div>
|
||||||
<div class="sp-box-body">
|
<div class="sp-box-body">
|
||||||
<form class="sp-form" method="post" action="#">
|
<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="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>
|
<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">Where the panel writes to you: the notifications on the right and a password-reset link. It can also receive DMARC reports — your choice, under <a href="#dmarc">DMARC reports</a>.</p></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>
|
<button type="submit" class="button is-primary">Save profile</button>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="column is-6">
|
||||||
<div class="box">
|
<div class="box">
|
||||||
<div class="sp-box-head"><span class="sp-no">02</span><h2>Password</h2></div>
|
<div class="sp-box-head"><span class="sp-no">02</span><h2>Password</h2></div>
|
||||||
<div class="sp-box-body">
|
<div class="sp-box-body">
|
||||||
@@ -36,53 +38,4 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="column is-7">
|
|
||||||
<div class="box">
|
|
||||||
<div class="sp-box-head"><span class="sp-no">03</span><h2>Notifications</h2><div class="sp-end">Sent to mix@example.org</div></div>
|
|
||||||
<div class="sp-box-body">
|
|
||||||
<form class="sp-form" method="post" action="#">
|
|
||||||
<p class="sp-kicker mb-2">Your domains</p>
|
|
||||||
<div class="field"><label class="checkbox"><input type="checkbox" name="ev" checked> A DNS record stops matching — DKIM, SPF, DMARC or an inbound MX</label></div>
|
|
||||||
<div class="field"><label class="checkbox"><input type="checkbox" name="ev" checked> A domain or an application reaches its rate limit and mail is being refused</label></div>
|
|
||||||
<div class="field"><label class="checkbox"><input type="checkbox" name="ev" checked> An inbound domain's upstream cannot be reached and mail is piling up</label></div>
|
|
||||||
<div class="field"><label class="checkbox"><input type="checkbox" name="ev"> DMARC reports start showing failures for a domain</label></div>
|
|
||||||
<div class="field"><label class="checkbox"><input type="checkbox" name="ev"> Mail is held in quarantine</label></div>
|
|
||||||
<p class="sp-kicker mt-4 mb-2">The server · global role</p>
|
|
||||||
<div class="field"><label class="checkbox"><input type="checkbox" name="ev" checked> The TLS certificate is within 14 days of expiry</label></div>
|
|
||||||
<div class="field"><label class="checkbox"><input type="checkbox" name="ev" checked> A process or a milter socket stops answering</label></div>
|
|
||||||
<div class="field"><label class="checkbox"><input type="checkbox" name="ev"> Messages sit in a queue for more than a day</label></div>
|
|
||||||
<p class="help mb-3">One message when a problem starts and one when it clears — never a stream. A domain administrator is told only about the domains assigned to them.</p>
|
|
||||||
<button type="submit" class="button is-primary">Save notifications</button>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="box" id="dmarc">
|
|
||||||
<div class="sp-box-head"><span class="sp-no">04</span><h2>DMARC reports</h2><div class="sp-end"><span class="tag is-success is-light">authorized</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-record">
|
|
||||||
<form class="sp-form" method="post" action="#">
|
|
||||||
<div class="field"><label class="label" for="rua">My default report address</label>
|
|
||||||
<div class="control"><div class="select is-fullwidth"><select id="rua" name="dmarc_default">
|
|
||||||
<option>SelfPost hosted — read them under DMARC reports</option>
|
|
||||||
<option selected>My account e-mail — mix@example.org</option>
|
|
||||||
<option>Another address…</option>
|
|
||||||
<option>No reports</option>
|
|
||||||
</select></div></div>
|
|
||||||
<p class="help">Used by every outbound domain whose report address is set to <em>admin's default</em> — 2 of your 3 now. Changing it changes the DMARC record those domains must publish. The hosted address is parsed by SelfPost; any other just receives the raw XML.</p></div>
|
|
||||||
<button type="submit" class="button is-primary">Save report address</button>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
<div class="sp-record">
|
|
||||||
<div class="sp-record-head"><h3>Report authorization</h3><span class="tag is-success is-light">published</span><small>Needed because the address is on another domain than the senders</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="*._report._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"></div><div class="control"><button type="button" class="button"><i class="ti ti-copy"></i>Copy</button></div></div></div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -10,7 +10,7 @@
|
|||||||
<div class="columns">
|
<div class="columns">
|
||||||
<div class="column is-6">
|
<div class="column is-6">
|
||||||
<div class="box">
|
<div class="box">
|
||||||
<div class="sp-box-head"><span class="sp-no">01</span><h2>Full backup</h2></div>
|
<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">
|
<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>
|
<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="#">
|
<form class="sp-form" method="post" action="#">
|
||||||
@@ -28,9 +28,9 @@
|
|||||||
|
|
||||||
<div class="column is-6">
|
<div class="column is-6">
|
||||||
<div class="box" id="import">
|
<div class="box" id="import">
|
||||||
<div class="sp-box-head"><span class="sp-no">02</span><h2>Import a domain</h2></div>
|
<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">
|
<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#export">domain's settings</a>.</p>
|
<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">
|
<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="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="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>
|
||||||
|
|||||||
@@ -106,7 +106,7 @@
|
|||||||
<div class="table-container"><table class="table is-fullwidth is-hoverable">
|
<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>
|
<thead><tr><th>Time, UTC</th><th>Name</th><th>sp-clip</th><th>Status</th><th></th></tr></thead>
|
||||||
<tbody>
|
<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><a class="has-text-danger" href="#">Delete</a></td></tr>
|
<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>
|
</tbody>
|
||||||
</table></div>
|
</table></div>
|
||||||
<div class="sp-box-foot"><span>Page 1 of 9</span><a href="#">Older →</a></div>
|
<div class="sp-box-foot"><span>Page 1 of 9</span><a href="#">Older →</a></div>
|
||||||
|
|||||||
@@ -12,7 +12,7 @@
|
|||||||
<div class="columns">
|
<div class="columns">
|
||||||
<div class="column is-5">
|
<div class="column is-5">
|
||||||
<div class="box">
|
<div class="box">
|
||||||
<div class="sp-box-head"><span class="sp-no">01</span><h2>Sources · 7 days</h2></div>
|
<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">
|
<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>
|
<thead><tr><th>Source</th><th>Pass</th><th>Fail</th><th>Disposition</th></tr></thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
|
|||||||
@@ -10,7 +10,7 @@
|
|||||||
<div class="columns">
|
<div class="columns">
|
||||||
<div class="column is-6">
|
<div class="column is-6">
|
||||||
<div class="box">
|
<div class="box">
|
||||||
<div class="sp-box-head"><span class="sp-no">01</span><h2>Report</h2></div>
|
<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">
|
<div class="sp-box-body">
|
||||||
<dl class="sp-facts">
|
<dl class="sp-facts">
|
||||||
<div><dt>Reporter</dt><dd>Outlook.com</dd></div>
|
<div><dt>Reporter</dt><dd>Outlook.com</dd></div>
|
||||||
|
|||||||
@@ -8,12 +8,12 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="box">
|
<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></div>
|
<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">
|
<div class="sp-box-body">
|
||||||
<dl class="sp-facts">
|
<dl class="sp-facts">
|
||||||
<div><dt>Last report</dt><dd>2026-09-21 06:12 UTC</dd></div>
|
<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>This week</dt><dd>14 kept · 0 parse failures</dd></div>
|
||||||
<div><dt>Hosted address</dt><dd class="sp-mono">dmarc@mail.example.org</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>
|
<div><dt>Retention</dt><dd>500 reports or 90 days</dd></div>
|
||||||
</dl>
|
</dl>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -11,7 +11,7 @@
|
|||||||
<div class="columns">
|
<div class="columns">
|
||||||
<div class="column is-6">
|
<div class="column is-6">
|
||||||
<div class="box" id="machine">
|
<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></div>
|
<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">
|
<div class="table-container"><table class="table is-fullwidth">
|
||||||
<thead><tr><th>Resource</th><th>Usage</th><th>Detail</th></tr></thead>
|
<thead><tr><th>Resource</th><th>Usage</th><th>Detail</th></tr></thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
@@ -42,7 +42,7 @@
|
|||||||
<div class="columns">
|
<div class="columns">
|
||||||
<div class="column is-4">
|
<div class="column is-4">
|
||||||
<div class="box" id="certificate">
|
<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></div>
|
<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">
|
<div class="sp-box-body">
|
||||||
<dl class="sp-facts">
|
<dl class="sp-facts">
|
||||||
<div><dt>Expires</dt><dd class="sp-mono">2026-10-03 09:14 UTC</dd></div>
|
<div><dt>Expires</dt><dd class="sp-mono">2026-10-03 09:14 UTC</dd></div>
|
||||||
@@ -68,7 +68,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="column is-4">
|
<div class="column is-4">
|
||||||
<div class="box" id="hostname">
|
<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></div>
|
<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">
|
<div class="sp-box-body">
|
||||||
<dl class="sp-facts">
|
<dl class="sp-facts">
|
||||||
<div><dt>Hostname</dt><dd class="sp-mono">mail.example.org</dd></div>
|
<div><dt>Hostname</dt><dd class="sp-mono">mail.example.org</dd></div>
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
<div class="sp-grow">
|
<div class="sp-grow">
|
||||||
<p class="sp-kicker">Reference</p>
|
<p class="sp-kicker">Reference</p>
|
||||||
<h1 class="title is-3">Help</h1>
|
<h1 class="title is-3">Help</h1>
|
||||||
<p class="sp-lead">What each screen checks and what to do when it is not green. Every numbered section on a page links to its topic here.</p>
|
<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>
|
</div>
|
||||||
|
|
||||||
@@ -21,27 +21,61 @@
|
|||||||
<li><a href="#dns"><i class="ti ti-world-check"></i>DNS records</a></li>
|
<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="#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="#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="#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>
|
<li><a href="#dmarc"><i class="ti ti-report-analytics"></i>DMARC reports</a></li>
|
||||||
</ul>
|
</ul>
|
||||||
<p class="menu-label">Inbound</p>
|
<p class="menu-label">Inbound</p>
|
||||||
<ul class="menu-list">
|
<ul class="menu-list">
|
||||||
<li><a href="#inbound"><i class="ti ti-inbox"></i>Backup-MX and forwarding</a></li>
|
<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>
|
</ul>
|
||||||
</aside>
|
</aside>
|
||||||
</div>
|
</div>
|
||||||
<div class="column is-9">
|
<div class="column is-9">
|
||||||
<div class="box">
|
<div class="box">
|
||||||
<div class="sp-box-head"><span class="sp-no">01</span><h2>Overview</h2></div>
|
<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, 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.</p></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. It warns 14 days before expiry and fails once expired — clients that verify certificates then refuse to connect.</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 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 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>
|
||||||
<div class="box">
|
<div class="box">
|
||||||
<div class="sp-box-head"><span class="sp-no">02</span><h2>Outbound</h2></div>
|
<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.</p></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. It may send as any address of its domain or only as listed 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="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, manual or computed from 30-day statistics, and can never exceed level 1.</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>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -50,7 +50,7 @@
|
|||||||
<div class="columns">
|
<div class="columns">
|
||||||
<div class="column is-6">
|
<div class="column is-6">
|
||||||
<div class="box" id="upstream">
|
<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></div>
|
<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">
|
<div class="sp-box-body">
|
||||||
<form class="sp-form" method="post" action="#">
|
<form class="sp-form" method="post" action="#">
|
||||||
<div class="sp-row">
|
<div class="sp-row">
|
||||||
@@ -66,7 +66,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="column is-6">
|
<div class="column is-6">
|
||||||
<div class="box" id="recipients">
|
<div class="box" id="recipients">
|
||||||
<div class="sp-box-head"><span class="sp-no">03</span><h2>Valid recipients</h2></div>
|
<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">
|
<div class="sp-box-body">
|
||||||
<form class="sp-form" method="post" action="#">
|
<form class="sp-form" method="post" action="#">
|
||||||
<div class="field"><label class="label" for="rmode">Who to accept</label>
|
<div class="field"><label class="label" for="rmode">Who to accept</label>
|
||||||
@@ -85,7 +85,7 @@ security@lists.example.org</textarea></div>
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="box" id="filter">
|
<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></div>
|
<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">
|
<div class="sp-box-body">
|
||||||
<dl class="sp-facts">
|
<dl class="sp-facts">
|
||||||
<div><dt>Engine</dt><dd>rspamd, for every inbound domain of this server</dd></div>
|
<div><dt>Engine</dt><dd>rspamd, for every inbound domain of this server</dd></div>
|
||||||
|
|||||||
@@ -21,7 +21,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="box">
|
<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></div>
|
<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">
|
<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>
|
<thead><tr><th>Domain</th><th>DNS</th><th>Upstream</th><th>Recipients</th><th>TLS to upstream</th><th></th></tr></thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
|
|||||||
@@ -0,0 +1,17 @@
|
|||||||
|
<!-- title: Sign in | shell: bare -->
|
||||||
|
<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>
|
||||||
@@ -31,7 +31,7 @@
|
|||||||
|
|
||||||
<div class="column is-4">
|
<div class="column is-4">
|
||||||
<div class="box">
|
<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></div>
|
<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="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="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="field"><label class="label" for="ips">Allowed client IPs</label>
|
||||||
@@ -43,7 +43,7 @@
|
|||||||
|
|
||||||
<div class="column is-4">
|
<div class="column is-4">
|
||||||
<div class="box">
|
<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></div>
|
<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="sp-box-body">
|
||||||
<div class="field"><label class="label" for="rmode">Limit mode</label>
|
<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="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>
|
||||||
|
|||||||
@@ -31,7 +31,7 @@
|
|||||||
<div class="box">
|
<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-head"><span class="sp-no"><i class="ti ti-file-export"></i></span><h2>Moving it instead?</h2></div>
|
||||||
<div class="sp-box-body">
|
<div class="sp-box-body">
|
||||||
<p><a href="out-domain-settings.html#export">Export the domain</a> first. The file carries the key and the application passwords, so the new instance needs no DNS change.</p>
|
<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>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -10,25 +10,25 @@
|
|||||||
<div class="columns">
|
<div class="columns">
|
||||||
<div class="column is-6">
|
<div class="column is-6">
|
||||||
<div class="box" id="reports">
|
<div class="box" id="reports">
|
||||||
<div class="sp-box-head"><span class="sp-no">01</span><h2>DMARC report address</h2></div>
|
<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">
|
<div class="sp-box-body">
|
||||||
<form class="sp-form" method="post" action="#">
|
<form class="sp-form" method="post" action="#">
|
||||||
<div class="field"><label class="label" for="rua">Aggregate reports (rua=)</label>
|
<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">
|
<div class="control"><div class="select is-fullwidth"><select id="rua" name="dmarc_rua_mode">
|
||||||
<option selected>admin's default — mix@example.org</option>
|
<option>No reports</option>
|
||||||
<option>SelfPost hosted (example.org@dmarc.mail.example.org)</option>
|
<option>SelfPost hosted — example.org@dmarc.mail.example.org</option>
|
||||||
<option>My default instead — (you are admin)</option>
|
<option selected>A specific address</option>
|
||||||
<option>No aggregate reports</option>
|
|
||||||
<option>Custom address</option>
|
|
||||||
</select></div></div>
|
</select></div></div>
|
||||||
<p class="help">A default belongs to a user and is set under their <a href="account.html#dmarc">Account</a>; a domain follows one named user, so two people sharing a domain never pull it two ways. Changing this changes the DMARC record to publish.</p></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>
|
<button type="submit" class="button is-primary">Save report address</button>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="box" id="export">
|
<div class="box" id="export">
|
||||||
<div class="sp-box-head"><span class="sp-no">03</span><h2>Export domain</h2></div>
|
<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">
|
<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>
|
<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="#">
|
<form class="sp-form" method="post" action="#">
|
||||||
|
|||||||
@@ -28,9 +28,9 @@
|
|||||||
</ul>
|
</ul>
|
||||||
<p class="menu-label">Rarely changed</p>
|
<p class="menu-label">Rarely changed</p>
|
||||||
<ul class="menu-list">
|
<ul class="menu-list">
|
||||||
<li><a href="out-domain-settings.html#reports"><i class="ti ti-mail-forward"></i>Report address</a></li>
|
<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#limit"><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-gauge"></i>Rate limit <span class="tag is-success is-light">auto</span></a></li>
|
||||||
<li><a href="out-domain-settings.html#export"><i class="ti ti-file-export"></i>Export</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>
|
<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>
|
</ul>
|
||||||
</aside>
|
</aside>
|
||||||
@@ -66,14 +66,14 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="sp-record">
|
<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#reports">admin's default</a></small></div>
|
<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="sp-row">
|
||||||
<div class="field"><label class="label">Host / name</label>
|
<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 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 class="field"><label class="label">Type</label><div class="control"><input class="input" readonly value="TXT"></div></div>
|
||||||
</div>
|
</div>
|
||||||
<div class="field"><label class="label">Value to publish</label>
|
<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"><i class="ti ti-copy"></i>Copy</button></div></div>
|
<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>
|
<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>
|
</div>
|
||||||
@@ -83,10 +83,10 @@
|
|||||||
<div class="table-container"><table class="table is-fullwidth is-hoverable">
|
<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>
|
<thead><tr><th>Login</th><th>May send as</th><th>30 days</th><th>Limits</th><th></th></tr></thead>
|
||||||
<tbody>
|
<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>
|
<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><a href="out-app-created.html">New password</a><a class="has-text-danger" href="#">Delete</a></td></tr>
|
<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"><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>
|
<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><a href="out-app-created.html">New password</a><a class="has-text-danger" href="#">Delete</a></td></tr>
|
<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>
|
</tbody>
|
||||||
</table></div>
|
</table></div>
|
||||||
<div class="sp-box-foot">New password and Delete ask for confirmation; the old password stops working at once.</div>
|
<div class="sp-box-foot">New password and Delete ask for confirmation; the old password stops working at once.</div>
|
||||||
|
|||||||
@@ -16,6 +16,7 @@
|
|||||||
<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>
|
<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>
|
<button type="submit" class="button"><i class="ti ti-filter"></i>Filter</button>
|
||||||
</form>
|
</form>
|
||||||
|
<a class="sp-help" href="help.html#log" title="What the statuses mean"><i class="ti ti-help-circle"></i></a>
|
||||||
</div>
|
</div>
|
||||||
<div class="table-container"><table class="table is-fullwidth is-hoverable">
|
<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>
|
<thead><tr><th>Time, UTC</th><th>From</th><th>To</th><th>Subject</th><th>Status</th><th></th></tr></thead>
|
||||||
|
|||||||
@@ -25,7 +25,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="column is-5">
|
<div class="column is-5">
|
||||||
<div class="box">
|
<div class="box">
|
||||||
<div class="sp-box-head"><span class="sp-no">02</span><h2>History</h2></div>
|
<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">
|
<div class="sp-box-body">
|
||||||
<ol class="sp-timeline">
|
<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><time>13:58:07 UTC</time><strong>Accepted</strong> from <span class="sp-mono">alerts</span> at 198.51.100.7</li>
|
||||||
|
|||||||
@@ -8,7 +8,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="box">
|
<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></div>
|
<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">
|
<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>
|
<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>
|
<tbody>
|
||||||
|
|||||||
@@ -12,12 +12,12 @@
|
|||||||
<div class="box">
|
<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-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">
|
<div class="sp-health">
|
||||||
<a href="health.html#machine"><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-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#processes"><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"><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#certificate" 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="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="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#sockets"><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-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#hostname"><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>
|
<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>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
<div class="sp-grow">
|
<div class="sp-grow">
|
||||||
<p class="sp-kicker">Server</p>
|
<p class="sp-kicker">Server</p>
|
||||||
<h1 class="title is-3">Settings</h1>
|
<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, notifications and DMARC report address are under <a href="account.html">Account</a>.</p>
|
<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>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -11,30 +11,22 @@
|
|||||||
<div class="columns">
|
<div class="columns">
|
||||||
<div class="column is-6">
|
<div class="column is-6">
|
||||||
<div class="box">
|
<div class="box">
|
||||||
<div class="sp-box-head"><span class="sp-no">01</span><h2>Log retention</h2></div>
|
<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="sp-box-body">
|
||||||
<div class="field"><label class="label" for="ret">Keep outbound log rows, days</label>
|
<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>
|
<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>
|
<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>
|
||||||
|
|
||||||
<div class="box">
|
|
||||||
<div class="sp-box-head"><span class="sp-no">02</span><h2>Notifications</h2></div>
|
|
||||||
<div class="sp-box-body">
|
|
||||||
<div class="field"><label class="checkbox"><input type="checkbox" name="notify" checked> Send notification e-mails</label></div>
|
|
||||||
<p class="help">Sent through this relay from <span class="sp-mono">postmaster@mail.example.org</span>. Each user picks the events under <a href="account.html">Account</a>; a user without an e-mail gets none.</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="column is-6">
|
<div class="column is-6">
|
||||||
<div class="box" id="rate-limits">
|
<div class="box" id="rate-limits">
|
||||||
<div class="sp-box-head"><span class="sp-no">03</span><h2>Sending rate limits</h2><div class="sp-end">Read-only</div></div>
|
<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">
|
<div class="sp-box-body">
|
||||||
<dl class="sp-facts">
|
<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 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#limit">domain</a> and <a href="out-app.html">application</a>.</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>
|
</dl>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -12,7 +12,7 @@
|
|||||||
<div class="columns">
|
<div class="columns">
|
||||||
<div class="column is-6">
|
<div class="column is-6">
|
||||||
<div class="box">
|
<div class="box">
|
||||||
<div class="sp-box-head"><span class="sp-no">01</span><h2>Account</h2></div>
|
<div class="sp-box-head"><span class="sp-no">01</span><h2>Account</h2><a class="sp-help" href="help.html#users" title="Roles and domain assignments"><i class="ti ti-help-circle"></i></a></div>
|
||||||
<div class="sp-box-body">
|
<div class="sp-box-body">
|
||||||
<div class="field"><label class="label" for="username">Username</label><div class="control"><input class="input" id="username" name="username" value="acme-ops" autocomplete="username" spellcheck="false" required></div></div>
|
<div class="field"><label class="label" for="username">Username</label><div class="control"><input class="input" id="username" name="username" value="acme-ops" autocomplete="username" spellcheck="false" required></div></div>
|
||||||
<div class="field"><label class="label" for="email">E-mail</label><div class="control"><input class="input" id="email" name="email" type="email" value="ops@acme.io" autocomplete="off"></div>
|
<div class="field"><label class="label" for="email">E-mail</label><div class="control"><input class="input" id="email" name="email" type="email" value="ops@acme.io" autocomplete="off"></div>
|
||||||
|
|||||||