Compare commits
68
Commits
faa3c11ae7
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7289aec60b | ||
|
|
0eaf69a830 | ||
|
|
2bf9056d20 | ||
|
|
9b33b805d8 | ||
|
|
de4274ccc9 | ||
|
|
1eec1c6011 | ||
|
|
9aebe1efe4 | ||
|
|
3d23c41ae7 | ||
|
|
b8e156c9f4 | ||
|
|
09edc0fac6 | ||
|
|
b7bbb2ca14 | ||
|
|
2f5f5b2b93 | ||
|
|
b7f04217f2 | ||
|
|
ddafc96bab | ||
|
|
f1cc3f3b37 | ||
|
|
c5edbd84c7 | ||
|
|
be98efa549 | ||
|
|
2dded921f5 | ||
|
|
bdeb01681d | ||
|
|
c205106418 | ||
|
|
4ff032a463 | ||
|
|
f2ff8b4fea | ||
|
|
8f0ccf90cb | ||
|
|
75f97c9915 | ||
|
|
e3b25010b2 | ||
|
|
e92769f833 | ||
|
|
546a1aa461 | ||
|
|
a8fd96c752 | ||
|
|
0feaa552d1 | ||
|
|
cc33890523 | ||
|
|
b2dcefc920 | ||
|
|
a1724ad72c | ||
|
|
503939c6cf | ||
|
|
af778445d4 | ||
|
|
49c872f836 | ||
|
|
1888dce1d4 | ||
|
|
dd99e0f90b | ||
|
|
3ba0e9c691 | ||
|
|
e0e30e284a | ||
|
|
1dc3b7e5fe | ||
|
|
22ce1bdfbb | ||
|
|
f4f6e0f23d | ||
|
|
d51549385b | ||
|
|
46b90d2a8d | ||
|
|
6531db977e | ||
|
|
bd21ff0f90 | ||
|
|
216f366dfa | ||
|
|
295d72e721 | ||
|
|
5348330d2b | ||
|
|
446feadd74 | ||
|
|
9420d8c03d | ||
|
|
3f5cd9adf0 | ||
|
|
52a9811baa | ||
|
|
98e4dfcda4 | ||
|
|
82a53e111c | ||
|
|
f95c5530c7 | ||
|
|
51f303c466 | ||
|
|
fffee9cb5d | ||
|
|
1af0d3f1c8 | ||
|
|
ac21180d58 | ||
|
|
ca3a1834da | ||
|
|
f1d90a2a1b | ||
|
|
80faf1a901 | ||
|
|
0cceb549cc | ||
|
|
d9d9af3932 | ||
|
|
2cfd239d16 | ||
|
|
f0dd9f943e | ||
|
|
c2f63d2789 |
+307
-4
@@ -5,6 +5,279 @@ Format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/); version
|
|||||||
|
|
||||||
## [Unreleased]
|
## [Unreleased]
|
||||||
|
|
||||||
|
## [2.14.2] - 2026-10-17
|
||||||
|
|
||||||
|
The last small items of the day. No migration; schema 13.
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- **Health dials the inbound journal's socket** with the inbound relay on:
|
||||||
|
the milter that writes Inbound › Log was the one the panel did not check.
|
||||||
|
The send log's socket is named as the mockup names it, `journal-milter`.
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- **A DMARC report says which day it is**: its day stays the reporter's, in
|
||||||
|
UTC (owner, 2026-10-17: accepted as it is), and the report's page now says
|
||||||
|
so under its window; Settings and Help name it as the one exception to
|
||||||
|
the panel's zone. A daily report from midnight to midnight is one day, not
|
||||||
|
two.
|
||||||
|
|
||||||
|
### Documentation
|
||||||
|
|
||||||
|
- The guide's gaps from the docs audit: `/license` and `/server/components`,
|
||||||
|
DMARC pages' paths, `selfpost-backup -o`, `-i` and `-version`, the
|
||||||
|
Inbound log's retention field, `PANEL_COOKIE_SECURE`.
|
||||||
|
|
||||||
|
## [2.14.1] - 2026-10-17
|
||||||
|
|
||||||
|
Overview's head in the mockup's words. No migration; schema 13.
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- **Overview's head says it as the mockup does** (overview-head): "Mail is
|
||||||
|
flowing. One thing needs attention." - or "Nothing needs attention", or
|
||||||
|
"Mail may not be flowing" when a check fails - and under it each check
|
||||||
|
that is not fine with its value ("TLS certificate: expires in 12 days.").
|
||||||
|
|
||||||
|
## [2.14.0] - 2026-10-17
|
||||||
|
|
||||||
|
Password reset by e-mail, to a confirmed address. Migration 0013; schema 13.
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- **Password reset by e-mail** (password-reset): *Forgot your password?* on
|
||||||
|
the sign-in page sends a one-time link, for 30 minutes, to the account's
|
||||||
|
e-mail - once that address has been confirmed by a link sent to it. A new
|
||||||
|
address is sent its confirmation link when it is saved (Account, or the
|
||||||
|
user form); Account shows whether it is confirmed and can send the link
|
||||||
|
again. Notifications go to the address confirmed or not. Setting a new
|
||||||
|
password from a link signs every session of the account out. Migration
|
||||||
|
0013; schema 13.
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- **Changing your e-mail on Account asks for your password**, as changing
|
||||||
|
the password does: the address is where a reset link goes.
|
||||||
|
|
||||||
|
## [2.13.1] - 2026-10-17
|
||||||
|
|
||||||
|
The documents and the panel's words checked against the code. No migration;
|
||||||
|
schema 12.
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- **A panel password's twelve characters are characters**: the minimum
|
||||||
|
counted bytes, so six letters of a non-Latin alphabet passed for twelve.
|
||||||
|
It counts characters now, as the backup password always did. Passwords
|
||||||
|
already set are not affected.
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- **The panel and its mockups say the same** (docs-audit): an inbound
|
||||||
|
domain's MX box says when it was checked and its side menu links the
|
||||||
|
domain's Inbound log; a deferred message's history ends with the wait in
|
||||||
|
the queue; Overview says it refreshes on its own; DMARC's hint for an
|
||||||
|
unaligned third party reads as the mockup. Help says what the code does:
|
||||||
|
an application's password is kept by the server so an export can carry
|
||||||
|
it, every password check of a backup counts, a password set for another
|
||||||
|
user signs them out everywhere, the limit mail is daily per sending
|
||||||
|
domain, and a domain administrator is no longer shown the Filter lists
|
||||||
|
topic (their page is the global role's).
|
||||||
|
|
||||||
|
### Documentation
|
||||||
|
|
||||||
|
- The documents checked against the code (docs-audit, 2026-10-17): the
|
||||||
|
guide, architecture, schema-migrations, security, development, README and
|
||||||
|
product say what the code does.
|
||||||
|
|
||||||
|
## [2.13.0] - 2026-10-17
|
||||||
|
|
||||||
|
The panel in the container's time zone. No migration; schema 12.
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- **The panel in your time zone** (local-time): set `TZ` in `.env` (an IANA
|
||||||
|
name, e.g. `Europe/Moscow`) and every time the panel shows, every day it
|
||||||
|
counts - today on Health, the statistics' days, the days of the log
|
||||||
|
filters - and the stamps of `mail.log` are that zone's. The screens no
|
||||||
|
longer write "UTC" beside a time; **Server › Settings** names the zone in
|
||||||
|
a new read-only box. Unset, it is UTC as before. Changing it is a
|
||||||
|
restart; days counted before stay the old zone's.
|
||||||
|
|
||||||
|
## [2.12.0] - 2026-10-17
|
||||||
|
|
||||||
|
A test message from Server › Health. No migration; schema 12.
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- **A test message from Server › Health** (test-email): one message to an
|
||||||
|
address you type, from `postmaster@` the hostname, the way notifications
|
||||||
|
go. The page that opens is its row in the Outbound log, which follows it
|
||||||
|
to the receiving server. It tests the server's own path, not a domain's.
|
||||||
|
Global role, ten an hour.
|
||||||
|
|
||||||
|
## [2.11.0] - 2026-10-16
|
||||||
|
|
||||||
|
Health checks the certificate's name, Postfix's greeting and the reverse
|
||||||
|
proxy. No migration; schema 12.
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- **Server › Health checks three things it did not** (health-setup-checks):
|
||||||
|
whether the TLS certificate is for the server's own name - a tag beside
|
||||||
|
its names, and a failure when it is not, since a client that checks the
|
||||||
|
name refuses it however valid it is; Postfix's greeting, the `220` line,
|
||||||
|
which must name the hostname; and the address the panel sees your browser
|
||||||
|
at, warning when a reverse proxy it does not trust stands in front - every
|
||||||
|
visitor then shares one sign-in limit - and naming what to put in
|
||||||
|
`TRUSTED_PROXY_CIDR`.
|
||||||
|
|
||||||
|
## [2.10.4] - 2026-10-16
|
||||||
|
|
||||||
|
The origin check is strict. No migration; schema 12.
|
||||||
|
|
||||||
|
### Security
|
||||||
|
|
||||||
|
- **A change with no word of where it came from is refused**
|
||||||
|
(origin-strict): a `POST` that carries neither `Sec-Fetch-Site` nor
|
||||||
|
`Origin` is answered 403, as a cross-origin one is. It was let through
|
||||||
|
before, as an accepted risk, which left a browser that sends neither open
|
||||||
|
to a request forged by any site. Every current browser sends `Origin` with
|
||||||
|
a form; a browser too old to (Firefox before 70) can no longer save
|
||||||
|
anything in the panel.
|
||||||
|
|
||||||
|
## [2.10.3] - 2026-10-16
|
||||||
|
|
||||||
|
The backup asks for your password. No migration; schema 12.
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- **A backup and a domain's export ask for your password** (backup-reauth):
|
||||||
|
both downloads take the signed-in user's account password, checked like a
|
||||||
|
password change and under its limit, so a stolen session or a panel left
|
||||||
|
open no longer hands over the server's every secret.
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- **The file field of a domain's import is drawn as designed**: Bulma's
|
||||||
|
file button and the name of the file chosen, not the browser's own
|
||||||
|
"Browse…".
|
||||||
|
|
||||||
|
## [2.10.2] - 2026-10-16
|
||||||
|
|
||||||
|
The panel on a phone. No migration; schema 12.
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- **A narrow screen gets a Menu** (phone-desktop-mode): below 1024 px - a
|
||||||
|
phone, a phone in "desktop site" mode, a tablet, a narrow window - the
|
||||||
|
navigation was a sheet of every section and page, always open, filling
|
||||||
|
the first screen. It is now *Menu* at the right of the header, which opens
|
||||||
|
the sections as folds, the current one open, the account's entries last;
|
||||||
|
no script. The page no longer meets the screen's edge. From 1024 px
|
||||||
|
nothing changes.
|
||||||
|
|
||||||
|
## [2.10.1] - 2026-10-16
|
||||||
|
|
||||||
|
Postfix's queue ids no longer repeat. No migration; schema 12.
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- **Queue ids no longer repeat**: Postfix makes long, non-repeating queue ids
|
||||||
|
(`enable_long_queue_ids=yes`). A short id - the microsecond of arrival and
|
||||||
|
the queue file's inode - came round once the inode did, and a later message
|
||||||
|
could then rewrite an older row of the Outbound log with the same id and
|
||||||
|
recipient, put its lines on that row's page, or reach the Inbound log's
|
||||||
|
row of an earlier message. New ids read like `3Pt2mN2VXxznjll`; mail
|
||||||
|
already in the queue keeps its short id; Outbound › Queue's Queue ID
|
||||||
|
column is wider.
|
||||||
|
|
||||||
|
## [2.10.0] - 2026-10-16
|
||||||
|
|
||||||
|
Inbound › Log says what became of accepted mail: the upstream's answer,
|
||||||
|
its last reply and the host that gave it. Migration `0012`; schema 12.
|
||||||
|
Nothing for an operator to do.
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- **Inbound › Log shows what the upstream answered** (inbound-upstream-result,
|
||||||
|
[the plan](docs/plans/inbound-upstream-result.md)): an *Upstream* column
|
||||||
|
beside the decision - *queued*, *held*, *sent*, *deferred* (a greylisting, an
|
||||||
|
upstream that cannot be reached), *bounced* (refused, or expired in the
|
||||||
|
queue) - opening the last reply, the host that gave it and when. Read from
|
||||||
|
the delivery lines of `mail.log`, one update of the row per attempt; the
|
||||||
|
lines of port-25 mail are still not stored. Rows logged before have no
|
||||||
|
result. Migration `0012`, schema 12.
|
||||||
|
|
||||||
|
## [2.9.1] - 2026-10-16
|
||||||
|
|
||||||
|
Log panes read on a phone. No migration; schema 11.
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- **Log panes wrap their lines instead of scrolling sideways**: a phone drew
|
||||||
|
two scroll bars over the Delivery log of a message, and the end of a
|
||||||
|
delivery line - `status=` and the reply - was out of sight. A long line
|
||||||
|
wraps, the rest indented under its start; copied text keeps one line per
|
||||||
|
line. The Delivery log, System log and the queue's listing.
|
||||||
|
|
||||||
|
## [2.9.0] - 2026-10-16
|
||||||
|
|
||||||
|
The panel's DNS records read faster: a record DNS already holds as shown
|
||||||
|
is one line, its values on request. Server › Settings puts its Save button
|
||||||
|
under its own fields and can re-check the records of the server's own
|
||||||
|
name. `/data/opendkim`, which 2.8.x kept for a rollback to 2.7.x, is
|
||||||
|
removed at the first start; an installation still on 2.7.x goes through
|
||||||
|
2.8.x first (guide, § Upgrading). No migration; schema 11.
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- **A DNS record DNS already holds as shown is folded** (dns-record-collapse):
|
||||||
|
on an outbound domain's page, an inbound domain's and in Server ›
|
||||||
|
Settings › Notification mail, a *published* record is one line - its
|
||||||
|
name, its tag and *Show values*, which opens the host, type and value to
|
||||||
|
copy them again. A record that is missing, weak, wrong or not checked
|
||||||
|
stays open as before, with *In DNS now* where it differs. A `<details>`
|
||||||
|
block, no script; *Re-check DNS* draws the page again, so a record that
|
||||||
|
has just turned right comes back folded.
|
||||||
|
- **Server › Settings: *Save settings* sits under the fields it saves**
|
||||||
|
(settings-save-row): the form is the left column - Log retention,
|
||||||
|
Notifications, the button - and the right column holds what is not a
|
||||||
|
field, Notification mail and the read-only Sending rate limits. Before,
|
||||||
|
the button stood under both columns, after a long gap whenever the
|
||||||
|
records of the server's own name were open.
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- **Re-check DNS on Server › Settings** (notification-mail-recheck): the
|
||||||
|
records of the server's own name can be checked again where they are
|
||||||
|
shown, as a domain's can on its page; the Notification mail box says when
|
||||||
|
they were checked. Before, the only way was Re-check DNS on Server ›
|
||||||
|
Health, which nothing on Settings pointed to. `POST
|
||||||
|
/server/settings/dns-recheck`, global role.
|
||||||
|
|
||||||
|
### Removed
|
||||||
|
|
||||||
|
- **`/data/opendkim`, the key tree of 2.7.x** (panel-dkim-signing, as
|
||||||
|
planned for the release after 2.8.0): 2.8.x copied its keys to
|
||||||
|
`/data/dkim` and kept it for a rollback to 2.7.x; the first start of
|
||||||
|
this image removes it once `/data/dkim` holds a key, and says so. A
|
||||||
|
`/data` whose keys are under the old tree only - one that never ran
|
||||||
|
2.8.x - stops the start rather than give the server's own name a new
|
||||||
|
key: start 2.8.x once first (guide, § Upgrading). The copy and the
|
||||||
|
backup's exclusion of the tree went with it. A rollback from here goes
|
||||||
|
to 2.8.x; to 2.7.x only through a 2.7.x backup.
|
||||||
|
|
||||||
|
## [2.8.0] - 2026-10-16
|
||||||
|
|
||||||
|
The panel signs outgoing mail with DKIM itself, and OpenDKIM - a 2018 beta
|
||||||
|
with open 2026 CVEs - has left the image. Keys, selectors and DNS records do
|
||||||
|
not change; the first start copies the keys to `/data/dkim`. The sender
|
||||||
|
check holds a `Sender:` line too. Nothing for an operator to do; a rollback
|
||||||
|
to 2.7.x needs one command first (guide, § Upgrading). No migration;
|
||||||
|
schema 11.
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
|
|
||||||
- **The panel signs outgoing mail with DKIM itself** (panel-dkim-signing,
|
- **The panel signs outgoing mail with DKIM itself** (panel-dkim-signing,
|
||||||
@@ -15,6 +288,12 @@ Format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/); version
|
|||||||
once more, as OpenDKIM signed; strict. Before OpenDKIM left, both signed
|
once more, as OpenDKIM signed; strict. Before OpenDKIM left, both signed
|
||||||
every message on the e2e stand and agreed on `h=` and `bh=`; dkimpy - an
|
every message on the e2e stand and agreed on `h=` and `bh=`; dkimpy - an
|
||||||
independent implementation - verifies the panel's signature on every run.
|
independent implementation - verifies the panel's signature on every run.
|
||||||
|
Beyond what OpenDKIM signed: `Sender` and `List-Unsubscribe-Post` (RFC 8058
|
||||||
|
wants it signed for one-click unsubscribe), and the shown headers a message
|
||||||
|
has - Subject, Date, To, Cc, Reply-To, Sender - named once more, so that
|
||||||
|
one added on the way breaks the signature. A header section over 1 MiB is
|
||||||
|
refused (`552 5.3.4`); a body line over 998 bytes, which Postfix breaks on
|
||||||
|
delivery, is logged with its domain.
|
||||||
|
|
||||||
### Removed
|
### Removed
|
||||||
|
|
||||||
@@ -26,14 +305,22 @@ Format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/); version
|
|||||||
### Changed
|
### Changed
|
||||||
|
|
||||||
- **The DKIM keys moved to `/data/dkim`** and are the panel's alone (0600):
|
- **The DKIM keys moved to `/data/dkim`** and are the panel's alone (0600):
|
||||||
the first start copies them from `/data/opendkim`, which is left as it was
|
the first start copies them from `/data/opendkim`, which is kept, the
|
||||||
for a rollback to 2.7.x. Keys, selectors and DNS records do not change.
|
panel's alone as well, for a rollback to 2.7.x, and left out of backups.
|
||||||
The KeyTable and SigningTable are gone; a selector table the signer reads
|
Keys, selectors and DNS records do not change. The KeyTable and
|
||||||
takes their place. `OPENDKIM_DIR` and `OPENDKIM_SOCKET` are gone.
|
SigningTable are gone; a selector table the signer reads takes their
|
||||||
|
place, rewritten from the database at every start. `OPENDKIM_DIR` and
|
||||||
|
`OPENDKIM_SOCKET` are gone.
|
||||||
- **The sender check reads `From:` as a mail program does** and no longer as
|
- **The sender check reads `From:` as a mail program does** and no longer as
|
||||||
OpenDKIM did: the line must be one address at a domain the application
|
OpenDKIM did: the line must be one address at a domain the application
|
||||||
may send as, and the 256-byte limit of OpenDKIM's buffer goes. The key is
|
may send as, and the 256-byte limit of OpenDKIM's buffer goes. The key is
|
||||||
the login's, so no `From:` line can choose another tenant's.
|
the login's, so no `From:` line can choose another tenant's.
|
||||||
|
- **A `Sender:` line is held to the same rule as `From:`** when a message
|
||||||
|
has one - the address a mail program shows as "on behalf of". A message
|
||||||
|
with another domain's address there, or with two `Sender:` lines, is
|
||||||
|
refused with `550 5.7.1 Sender: …`, tagged `wrong Sender` in the
|
||||||
|
Outbound log. A system writing for people outside your domains puts its
|
||||||
|
own address in `From:` and theirs in `Reply-To:`.
|
||||||
|
|
||||||
### Fixed
|
### Fixed
|
||||||
|
|
||||||
@@ -41,6 +328,22 @@ Format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/); version
|
|||||||
it as quoted-printable for a server that does not announce 8BITMIME, after
|
it as quoted-printable for a server that does not announce 8BITMIME, after
|
||||||
it was signed; `disable_mime_output_conversion=yes` now delivers it as
|
it was signed; `disable_mime_output_conversion=yes` now delivers it as
|
||||||
signed. OpenDKIM's signatures were exposed to this too.
|
signed. OpenDKIM's signatures were exposed to this too.
|
||||||
|
- **A message of nothing but headers was held whole by every milter.** The
|
||||||
|
milter library (go-milter v0.4.1) kept each header of a message in a map
|
||||||
|
of its own that no milter of the panel read - up to the message size
|
||||||
|
limit, on port 25 from strangers too. The panel builds it patched not to
|
||||||
|
(`third_party/go-milter`).
|
||||||
|
- **A rotation of `mail.log` due at the container's start logged an error.**
|
||||||
|
It runs before Postfix does, and its `postfix reload` failed ("the Postfix
|
||||||
|
mail system is not running"); nothing was lost - the Postfix starting
|
||||||
|
next opens the new file - but logrotate reported a failure. It reloads
|
||||||
|
only a running Postfix now.
|
||||||
|
- **A start on a queue of an earlier image could stop at `postfix check`.**
|
||||||
|
The numbers the image's package accounts get depend on what else it
|
||||||
|
installs, and the queue under `/data` kept the old ones; `postsuper`, which
|
||||||
|
the check runs as the mail owner, could not read it. The entrypoint hands
|
||||||
|
the queue to Postfix's users by name before the check now. The first
|
||||||
|
rollout of this version to the test server met it and was rolled back.
|
||||||
|
|
||||||
## [2.7.0] - 2026-10-15
|
## [2.7.0] - 2026-10-15
|
||||||
|
|
||||||
|
|||||||
@@ -6,9 +6,9 @@
|
|||||||
|
|
||||||
Self-hosted SMTP relay with a web panel — one Docker image, no provider.
|
Self-hosted SMTP relay with a web panel — one Docker image, no provider.
|
||||||
|
|
||||||
Postfix, OpenDKIM, and a small Go panel run together under `supervisord`; you
|
Postfix and a small Go panel, which also signs outgoing mail with DKIM, run
|
||||||
configure domains, DKIM keys, and SASL applications once, then point your apps
|
together under `supervisord`; you configure domains, DKIM keys, and SASL
|
||||||
at the SMTP endpoint.
|
applications once, then point your apps at the SMTP endpoint.
|
||||||
|
|
||||||
SelfPost sends mail straight to the internet from **your own IP**, with
|
SelfPost sends mail straight to the internet from **your own IP**, with
|
||||||
per-domain DKIM signing. No mailboxes or webmail. An optional inbound relay
|
per-domain DKIM signing. No mailboxes or webmail. An optional inbound relay
|
||||||
@@ -27,11 +27,11 @@ send log and DNS checks in the panel, encrypted backups.
|
|||||||
- 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: a page of topics, opened from the `?` in the head of a box
|
- 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 submitting mail (optional; refused at MAIL FROM, not an authentication boundary)
|
||||||
- Web panel grouped by mail direction — Overview, Outbound (domains, log, queue, DMARC reports), Inbound, Server (health, system log, backup, users, settings)
|
- 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 per-application overrides
|
||||||
- Full-server backup and single-domain export/import (optional password encryption)
|
- Full-server backup and single-domain export/import (optional password encryption)
|
||||||
- Single Docker image; production data in a `./data` bind mount (the quick start below uses a named Docker volume instead)
|
- Single Docker image; production data in a `./data` bind mount (the quick start below uses a named Docker volume instead)
|
||||||
|
|
||||||
@@ -112,8 +112,8 @@ outbound mail will not reach the real internet without DNS, PTR, and port 25.
|
|||||||
> local image and pulls nothing:
|
> local image and pulls nothing:
|
||||||
>
|
>
|
||||||
> ```sh
|
> ```sh
|
||||||
> docker build -f build/Dockerfile --build-arg VERSION=2.7.0 \
|
> docker build -f build/Dockerfile --build-arg VERSION=2.14.2 \
|
||||||
> -t ghcr.io/mixeme/selfpost:2.7.0 .
|
> -t ghcr.io/mixeme/selfpost:2.14.2 .
|
||||||
> ```
|
> ```
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
@@ -122,7 +122,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:2.7.0
|
ghcr.io/mixeme/selfpost:2.14.2
|
||||||
```
|
```
|
||||||
|
|
||||||
**Get the setup URL** (pick one):
|
**Get the setup URL** (pick one):
|
||||||
|
|||||||
+2
-1
@@ -70,7 +70,8 @@ These are the operator's responsibility or accepted trade-offs, documented in
|
|||||||
- Deliverability outcomes: mail rejected or filtered by a receiving provider is
|
- Deliverability outcomes: mail rejected or filtered by a receiving provider is
|
||||||
a policy decision of that provider, not a defect
|
a policy decision of that provider, not a defect
|
||||||
- Denial of service through sheer volume against a single-tenant relay
|
- Denial of service through sheer volume against a single-tenant relay
|
||||||
- Vulnerabilities in upstream Postfix, OpenDKIM, or the base image — report
|
- Vulnerabilities in upstream Postfix, the Go libraries SelfPost builds on,
|
||||||
|
or the base image — report
|
||||||
those upstream; if SelfPost's configuration makes an upstream issue
|
those upstream; if SelfPost's configuration makes an upstream issue
|
||||||
exploitable when it otherwise would not be, that *is* in scope
|
exploitable when it otherwise would not be, that *is* in scope
|
||||||
|
|
||||||
|
|||||||
+9
-1
@@ -13,8 +13,11 @@ WORKDIR /src
|
|||||||
# Version stamped into both binaries; MUST match the image tag.
|
# Version stamped into both binaries; MUST match the image tag.
|
||||||
ARG VERSION=dev
|
ARG VERSION=dev
|
||||||
|
|
||||||
# Module metadata first for layer caching.
|
# Module metadata first for layer caching. third_party/go-milter is a module
|
||||||
|
# go.mod replaces go-milter with (third_party/go-milter/PATCHES.md): the
|
||||||
|
# download needs it.
|
||||||
COPY go.mod go.sum ./
|
COPY go.mod go.sum ./
|
||||||
|
COPY third_party ./third_party
|
||||||
RUN go mod download
|
RUN go mod download
|
||||||
|
|
||||||
COPY cmd ./cmd
|
COPY cmd ./cmd
|
||||||
@@ -61,6 +64,10 @@ ENV DEBIAN_FRONTEND=noninteractive
|
|||||||
# than with the whole of systemd, the first alternative of its "Depends:
|
# than with the whole of systemd, the first alternative of its "Depends:
|
||||||
# systemd | systemd-standalone-sysusers | systemd-sysusers" (dpkg -s in the
|
# systemd | systemd-standalone-sysusers | systemd-sysusers" (dpkg -s in the
|
||||||
# image, 2026-10-15). Nothing here runs systemd.
|
# image, 2026-10-15). Nothing here runs systemd.
|
||||||
|
# tzdata is named for TZ (local-time): the zone of every time the panel shows
|
||||||
|
# and of the stamps Postfix writes in mail.log. supervisor's python3.13 brings
|
||||||
|
# it today (apt-cache rdepends --installed tzdata in the image, 2026-10-17);
|
||||||
|
# a TZ the image did not have would leave both in UTC without a word.
|
||||||
RUN echo "postfix postfix/mailname string localhost" | debconf-set-selections \
|
RUN echo "postfix postfix/mailname string localhost" | debconf-set-selections \
|
||||||
&& echo "postfix postfix/main_mailer_type string Internet Site" | debconf-set-selections \
|
&& echo "postfix postfix/main_mailer_type string Internet Site" | debconf-set-selections \
|
||||||
&& apt-get update \
|
&& apt-get update \
|
||||||
@@ -75,6 +82,7 @@ RUN echo "postfix postfix/mailname string localhost" | debconf-set-selections \
|
|||||||
openssl \
|
openssl \
|
||||||
curl \
|
curl \
|
||||||
systemd-standalone-sysusers \
|
systemd-standalone-sysusers \
|
||||||
|
tzdata \
|
||||||
&& rm -rf /var/lib/apt/lists/*
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
# Debian's postfix runs its chroot sync on every "postfix check" and "postfix
|
# Debian's postfix runs its chroot sync on every "postfix check" and "postfix
|
||||||
|
|||||||
+60
-40
@@ -87,7 +87,7 @@ drop_links() {
|
|||||||
done
|
done
|
||||||
}
|
}
|
||||||
drop_links /data/dkim /data/dkim.new /data/opendkim /data/sasl /data/postfix /data/antispam /data/log \
|
drop_links /data/dkim /data/dkim.new /data/opendkim /data/sasl /data/postfix /data/antispam /data/log \
|
||||||
/data/dkim/keys /data/dkim/selectors /data/opendkim/keys \
|
/data/dkim/keys /data/dkim/selectors \
|
||||||
/data/sasl/sasldb2 /data/postfix/queue \
|
/data/sasl/sasldb2 /data/postfix/queue \
|
||||||
/data/postfix/sender_login_maps /data/postfix/relay_domains \
|
/data/postfix/sender_login_maps /data/postfix/relay_domains \
|
||||||
/data/postfix/transport /data/postfix/relay_recipients /data/postfix/tls_policy \
|
/data/postfix/transport /data/postfix/relay_recipients /data/postfix/tls_policy \
|
||||||
@@ -96,39 +96,40 @@ drop_links /data/dkim /data/dkim.new /data/opendkim /data/sasl /data/postfix /da
|
|||||||
# under /data; make sure they stay writable without disturbing anything that a
|
# under /data; make sure they stay writable without disturbing anything that a
|
||||||
# later phase deliberately hands to another service. /data/log is exempt: it is
|
# later phase deliberately hands to another service. /data/log is exempt: it is
|
||||||
# deliberately owned by postfix (postlogd writes the delivery log there) and is
|
# deliberately owned by postfix (postlogd writes the delivery log there) and is
|
||||||
# normalised on its own below, as are the trees shared through group selfpost
|
# normalised on its own below, as are the trees shared through group selfpost.
|
||||||
# and the one OpenDKIM read until 2.8.0, which is left as it was (below).
|
|
||||||
# A file's owner is a number: an entry whose user or group is not the panel's
|
# A file's owner is a number: an entry whose user or group is not the panel's
|
||||||
# by that number - unpacked by root, or written under another image whose
|
# by that number - unpacked by root, or written under another image whose
|
||||||
# image gave the panel other ids (999:999 until 2.7.0, 10999:10999 since) - is
|
# image gave the panel other ids (999:999 until 2.7.0, 10999:10999 since) - is
|
||||||
# handed over whole.
|
# handed over whole.
|
||||||
find /data -mindepth 1 -maxdepth 1 ! -name log ! -name postfix ! -name opendkim ! -name sasl ! -name antispam \
|
find /data -mindepth 1 -maxdepth 1 ! -name log ! -name postfix ! -name sasl ! -name antispam \
|
||||||
\( ! -user panel -o ! -group panel \) -exec chown -R panel:panel {} +
|
\( ! -user panel -o ! -group panel \) -exec chown -R panel:panel {} +
|
||||||
|
|
||||||
# DKIM keys and the selector table, /data/dkim: the panel's alone - it makes
|
# DKIM keys and the selector table, /data/dkim: the panel's alone - it makes
|
||||||
# the keys, writes the table and signs (internal/domain, Keys; internal/milter,
|
# the keys, writes the table and signs (internal/domain, Keys; internal/milter,
|
||||||
# sign.go). Until 2.8.0 the keys were under /data/opendkim, shared with
|
# sign.go). The selector table is written by the panel before its signer
|
||||||
# OpenDKIM through group selfpost. The first start without /data/dkim copies
|
# serves (cmd/panel/signer.go). Until 2.8.0 the keys were under
|
||||||
# them over and leaves the old tree as it was, for an image of 2.7.x rolled
|
# /data/opendkim, shared with OpenDKIM; 2.8.x copied them to /data/dkim at its
|
||||||
# back to (docs/plans/panel-dkim-signing.md, Q2). The copy is made as the
|
# first start and kept the old tree for a rollback to 2.7.x until the next
|
||||||
# panel user, which can read nothing there the panel could not, into a
|
# release, this one (docs/plans/panel-dkim-signing.md, Q2). It is removed
|
||||||
# directory beside, renamed into place when it is whole: a start that stops
|
# once /data/dkim holds a key. A /data that never ran 2.8.x - keys under the
|
||||||
# midway copies again. A copy that fails stops the start (set -e) - the
|
# old tree only - stops the start instead: the panel would make the server's
|
||||||
# panel would otherwise make the server's own name a new key, and its
|
# own name a new key, and its published record would no longer match; 2.8.x
|
||||||
# published record would no longer match. The selector table is written by
|
# copies them (guide § Upgrading). rm -rf removes a link, not what it points
|
||||||
# the panel before its signer serves (cmd/panel/signer.go).
|
# to, and follows none inside (GNU coreutils rm(1)); a link at the path is
|
||||||
# (util-linux 2.41, setpriv(1): --reuid, --regid, --clear-groups; cp(1)
|
# gone already (drop_links). (e2e TestStartsOnA28Data, TestRefusesA27Data.)
|
||||||
# -P: never follow a symbolic link in the source.)
|
has_keys() { [ -d "$1" ] && [ -n "$(find "$1" -type f -name '*.private' | head -n 1)" ]; }
|
||||||
rm -rf /data/dkim.new
|
if [ -d /data/opendkim ]; then
|
||||||
if [ ! -e /data/dkim ] && [ -d /data/opendkim/keys ]; then
|
if ! has_keys /data/dkim/keys && has_keys /data/opendkim/keys; then
|
||||||
setpriv --reuid=panel --regid=panel --clear-groups -- \
|
echo "entrypoint: the DKIM keys are only under /data/opendkim, as 2.7.x kept them; start 2.8.x once first - it copies them to /data/dkim - then this image (guide, § Upgrading)" >&2
|
||||||
sh -c 'mkdir /data/dkim.new && cp -RP /data/opendkim/keys /data/dkim.new/keys'
|
exit 1
|
||||||
mv /data/dkim.new /data/dkim
|
fi
|
||||||
echo "entrypoint: the DKIM keys were copied from /data/opendkim to /data/dkim; /data/opendkim is left for a rollback" >&2
|
rm -rf /data/opendkim
|
||||||
|
echo "entrypoint: removed /data/opendkim, the key tree of 2.7.x; the keys are under /data/dkim" >&2
|
||||||
fi
|
fi
|
||||||
|
rm -rf /data/dkim.new
|
||||||
mkdir -p /data/dkim/keys
|
mkdir -p /data/dkim/keys
|
||||||
chown -R panel:panel /data/dkim
|
chown -R panel:panel /data/dkim
|
||||||
find /data/dkim -type d -exec chmod 0700 {} +
|
find /data/dkim -type d -exec chmod 0700 {} + -exec chmod g-s {} +
|
||||||
find /data/dkim -type f -exec chmod 0600 {} +
|
find /data/dkim -type f -exec chmod 0600 {} +
|
||||||
|
|
||||||
# Application SASL accounts. The panel (user `panel`) writes the
|
# Application SASL accounts. The panel (user `panel`) writes the
|
||||||
@@ -219,34 +220,53 @@ chmod 2750 /run/selfpost
|
|||||||
# (test(1), -S: true for any socket file, whether or not anything listens.)
|
# (test(1), -S: true for any socket file, whether or not anything listens.)
|
||||||
rm -f /run/selfpost/*.sock /run/selfpost/*.alive
|
rm -f /run/selfpost/*.sock /run/selfpost/*.alive
|
||||||
|
|
||||||
|
# The queue tree belongs to Postfix's users by name. Their numbers are what
|
||||||
|
# the package's install hands out and depend on what else the image installs:
|
||||||
|
# without opendkim (2.8.0) postfix went from uid 101 to 100 and postdrop from
|
||||||
|
# gid 103 to 102, and a queue of an earlier image kept the old numbers. So an
|
||||||
|
# existing tree is handed over before postfix-config.sh runs "postfix check",
|
||||||
|
# whose postsuper drops to the mail owner before it reads the queue and
|
||||||
|
# otherwise stops the start ("scan_dir_push: open directory defer: Permission
|
||||||
|
# denied", the test server, 2026-10-16; Postfix 3.10.13,
|
||||||
|
# src/postsuper/postsuper.c:1442, set_ugid(var_owner_uid, var_owner_gid);
|
||||||
|
# conf/postfix-script:342). After the check it is done again for a tree the
|
||||||
|
# check has just built.
|
||||||
|
own_queue() {
|
||||||
|
[ -d /data/postfix/queue/active ] || return 0
|
||||||
|
chown -R postfix:postfix /data/postfix/queue
|
||||||
|
# The recursive chown 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. (Postfix 3.10.13, conf/postfix-files:51-68:
|
||||||
|
# $queue_directory root 755, maildrop $setgid_group 730, public
|
||||||
|
# $setgid_group 710, pid root 755, the other queue directories
|
||||||
|
# $mail_owner 700; conf/postfix-script:354-356, "check-warn": "not owned
|
||||||
|
# by root".)
|
||||||
|
chown root /data/postfix/queue /data/postfix/queue/pid
|
||||||
|
}
|
||||||
|
own_queue
|
||||||
|
|
||||||
# Generate the outbound-relay Postfix configuration from the environment (spec
|
# Generate the outbound-relay Postfix configuration from the environment (spec
|
||||||
# 5). Kept out of the image build so cert paths, rate limits, hostname and the
|
# 5). Kept out of the image build so cert paths, rate limits, hostname and the
|
||||||
# optional 587 service are all driven by env at run time, and re-derived on every
|
# optional 587 service are all driven by env at run time, and re-derived on every
|
||||||
# start the same way the /data normalisation above is.
|
# start the same way the /data normalisation above is.
|
||||||
/usr/local/bin/postfix-config.sh
|
/usr/local/bin/postfix-config.sh
|
||||||
|
|
||||||
# The queue tree, on a first start or after a restore, is already built here:
|
# The queue tree, on a first start or after a restore, is built here:
|
||||||
# postfix-config.sh ends with "postfix check", whose check-fatal runs
|
# postfix-config.sh ends with "postfix check", whose check-fatal runs
|
||||||
# "post-install create-missing" (Postfix 3.10.13, conf/postfix-script:314;
|
# "post-install create-missing" (Postfix 3.10.13, conf/postfix-script:314;
|
||||||
# conf/postfix-files:55, "$queue_directory/active:d:$mail_owner:-:700:ucr").
|
# conf/postfix-files:55, "$queue_directory/active:d:$mail_owner:-:700:ucr").
|
||||||
# A "postfix set-permissions" for a missing active/ stood here and never ran
|
# A "postfix set-permissions" for a missing active/ stood here and never ran
|
||||||
# (assumptions C-P34).
|
# (assumptions C-P34).
|
||||||
chown -R postfix:postfix /data/postfix/queue
|
own_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.
|
|
||||||
# (Postfix 3.10.13, conf/postfix-files:51-68: $queue_directory root 755, maildrop
|
|
||||||
# $setgid_group 730, public $setgid_group 710, pid root 755, the other queue
|
|
||||||
# directories $mail_owner 700; conf/postfix-script:354-356, "check-warn": "not
|
|
||||||
# owned by root".)
|
|
||||||
chown root /data/postfix/queue /data/postfix/queue/pid
|
|
||||||
|
|
||||||
# [program:postfix]'s startsecs (build/supervisord.conf): longer than the
|
# [program:postfix]'s startsecs (build/supervisord.conf): longer than the
|
||||||
# wrapper's wait for the milter sockets, so its exit at the end of the wait is
|
# wrapper's wait for the milter sockets, so its exit at the end of the wait is
|
||||||
|
|||||||
@@ -15,7 +15,15 @@
|
|||||||
compress
|
compress
|
||||||
delaycompress
|
delaycompress
|
||||||
create 0640 postfix selfpost
|
create 0640 postfix selfpost
|
||||||
|
# A rotation due when the container starts runs before Postfix does
|
||||||
|
# (logrotate-loop.sh runs at once, the wrapper waits for the milter
|
||||||
|
# sockets): no postlogd holds the old file then, the one Postfix starts
|
||||||
|
# opens the new, and a reload would only fail - "fatal: the Postfix mail
|
||||||
|
# system is not running", seen at the test server's rollout of
|
||||||
|
# 2026-10-16. "postfix status" exits 1 while no master runs (Postfix
|
||||||
|
# 3.10.13, conf/postfix-script:297-307), reload then nothing.
|
||||||
postrotate
|
postrotate
|
||||||
|
/usr/sbin/postfix status >/dev/null 2>&1 || exit 0
|
||||||
/usr/sbin/postfix reload
|
/usr/sbin/postfix reload
|
||||||
endscript
|
endscript
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -440,6 +440,19 @@ postconf -e \
|
|||||||
# panel's, were exposed to it as well.
|
# panel's, were exposed to it as well.
|
||||||
postconf -e "disable_mime_output_conversion=yes"
|
postconf -e "disable_mime_output_conversion=yes"
|
||||||
|
|
||||||
|
# Long, non-repeating queue ids (owner, 2026-10-16). The panel finds a
|
||||||
|
# message's rows, its stored lines and the inbound journal's upstream result
|
||||||
|
# by the queue id of mail.log; a short id is the microsecond of arrival and
|
||||||
|
# the queue file's inode in hex and repeats once the inode comes round, so a
|
||||||
|
# later message could meet an earlier one's rows. A long id adds the time in
|
||||||
|
# seconds and does not repeat. Postfix 3.10.13, src/global/mail_params.h:1010,
|
||||||
|
# DEF_LONG_QUEUE_IDS 0; src/global/mail_queue.h, MQID_SH_* and MQID_LG_*;
|
||||||
|
# proto/postconf.proto, enable_long_queue_ids: "Enable long, non-repeating,
|
||||||
|
# queue IDs", existing queue files keep their names, postqueue -p gets a
|
||||||
|
# wider Queue ID column. Every pattern of the panel takes both shapes
|
||||||
|
# (internal/logtail, internal/postfix/queue.go).
|
||||||
|
postconf -e "enable_long_queue_ids=yes"
|
||||||
|
|
||||||
# What Postfix tells a milter with MAIL FROM: its defaults plus the client's
|
# What Postfix tells a milter with MAIL FROM: its defaults plus the client's
|
||||||
# address. The panel's milters judge every message by that address (an
|
# address. The panel's milters judge every message by that address (an
|
||||||
# application's client-address list) and record it (the inbound journal), and
|
# application's client-address list) and record it (the inbound journal), and
|
||||||
|
|||||||
@@ -14,15 +14,17 @@ import (
|
|||||||
// are compared as they are, not as lists kept by hand beside them (audit
|
// are compared as they are, not as lists kept by hand beside them (audit
|
||||||
// 2026-10-12, stage 5 - the three lists this file held were compared with each
|
// 2026-10-12, stage 5 - the three lists this file held were compared with each
|
||||||
// other and with nothing else). Read: what the panel's code asks of the
|
// other and with nothing else). Read: what the panel's code asks of the
|
||||||
// environment (cmd/panel and internal, tests aside) and what the start-up
|
// environment (cmd/panel, cmd/selfpost-backup and internal, tests aside) and what the start-up
|
||||||
// scripts take with a default (build/*.sh, "${VAR:-…}"). Documented: the
|
// scripts take with a default (build/*.sh, "${VAR:-…}"). Documented: the
|
||||||
// operator's table in guide.md § Environment variables, with the paragraph
|
// operator's table in guide.md § Environment variables, with the paragraph
|
||||||
// under it, and the internal list in architecture.md § Configuration.
|
// under it, and the internal list in architecture.md § Configuration.
|
||||||
|
|
||||||
var (
|
var (
|
||||||
goEnvRead = regexp.MustCompile(`(?:envDefault|envInt|envBool|envSeconds|envDuration|os\.Getenv|os\.LookupEnv)\("([A-Z][A-Z0-9_]+)"`)
|
goEnvRead = regexp.MustCompile(`(?:envDefault|envInt|envBool|envSeconds|envDuration|os\.Getenv|os\.LookupEnv)\("([A-Z][A-Z0-9_]+)"`)
|
||||||
|
// A name kept in a constant, as the backup CLI keeps its password's.
|
||||||
|
goEnvConst = regexp.MustCompile(`\w+Env\s*=\s*"([A-Z][A-Z0-9_]+)"`)
|
||||||
scriptEnvRead = regexp.MustCompile(`\$\{([A-Z][A-Z0-9_]+):[-?=]`)
|
scriptEnvRead = regexp.MustCompile(`\$\{([A-Z][A-Z0-9_]+):[-?=]`)
|
||||||
docEnvName = regexp.MustCompile("`([A-Z][A-Z0-9_]{2,})`")
|
docEnvName = regexp.MustCompile("`([A-Z][A-Z0-9_]+)`")
|
||||||
)
|
)
|
||||||
|
|
||||||
func readFile(t *testing.T, path string) string {
|
func readFile(t *testing.T, path string) string {
|
||||||
@@ -38,12 +40,16 @@ func readFile(t *testing.T, path string) string {
|
|||||||
func envRead(t *testing.T) map[string]bool {
|
func envRead(t *testing.T) map[string]bool {
|
||||||
t.Helper()
|
t.Helper()
|
||||||
keys := map[string]bool{}
|
keys := map[string]bool{}
|
||||||
for _, root := range []string{".", filepath.Join("..", "..", "internal")} {
|
for _, root := range []string{".", filepath.Join("..", "selfpost-backup"), filepath.Join("..", "..", "internal")} {
|
||||||
err := filepath.WalkDir(root, func(path string, d fs.DirEntry, err error) error {
|
err := filepath.WalkDir(root, func(path string, d fs.DirEntry, err error) error {
|
||||||
if err != nil || d.IsDir() || !strings.HasSuffix(path, ".go") || strings.HasSuffix(path, "_test.go") {
|
if err != nil || d.IsDir() || !strings.HasSuffix(path, ".go") || strings.HasSuffix(path, "_test.go") {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
for _, m := range goEnvRead.FindAllStringSubmatch(readFile(t, path), -1) {
|
text := readFile(t, path)
|
||||||
|
for _, m := range goEnvRead.FindAllStringSubmatch(text, -1) {
|
||||||
|
keys[m[1]] = true
|
||||||
|
}
|
||||||
|
for _, m := range goEnvConst.FindAllStringSubmatch(text, -1) {
|
||||||
keys[m[1]] = true
|
keys[m[1]] = true
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
|
|||||||
@@ -112,10 +112,13 @@ func newPanel(cfg config, st *store.Store) (*web.Server, error) {
|
|||||||
TrustedProxyCIDRs: cfg.trustedProxies,
|
TrustedProxyCIDRs: cfg.trustedProxies,
|
||||||
TLSCertFile: cfg.tlsCertFile,
|
TLSCertFile: cfg.tlsCertFile,
|
||||||
TLSServedCertFile: cfg.tlsServedCertFile,
|
TLSServedCertFile: cfg.tlsServedCertFile,
|
||||||
|
SMTPBanner: cfg.smtpBanner,
|
||||||
|
TimeZone: cfg.timeZone,
|
||||||
TLSReloadInterval: cfg.tlsReloadInterval,
|
TLSReloadInterval: cfg.tlsReloadInterval,
|
||||||
SignSocket: cfg.signSocket,
|
SignSocket: cfg.signSocket,
|
||||||
SignLocalSocket: cfg.signLocalSocket,
|
SignLocalSocket: cfg.signLocalSocket,
|
||||||
JournalSocket: cfg.journalSocket,
|
JournalSocket: cfg.journalSocket,
|
||||||
|
InboundJournalSocket: cfg.inboundJournalSocket,
|
||||||
SenderSocket: cfg.senderSocket,
|
SenderSocket: cfg.senderSocket,
|
||||||
SessionIdleDays: cfg.sessionIdleDays,
|
SessionIdleDays: cfg.sessionIdleDays,
|
||||||
DNSResolvers: cfg.dnsResolvers,
|
DNSResolvers: cfg.dnsResolvers,
|
||||||
@@ -167,7 +170,9 @@ func serveHTTP(ctx context.Context, cfg config, st *store.Store) error {
|
|||||||
srv := newHTTPServer(cfg.httpAddr, srvApp.Handler())
|
srv := newHTTPServer(cfg.httpAddr, srvApp.Handler())
|
||||||
|
|
||||||
// Shut the server down cleanly when the process is asked to stop.
|
// Shut the server down cleanly when the process is asked to stop.
|
||||||
|
shutDown := make(chan struct{})
|
||||||
go func() {
|
go func() {
|
||||||
|
defer close(shutDown)
|
||||||
<-ctx.Done()
|
<-ctx.Done()
|
||||||
shutdownCtx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
shutdownCtx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||||
defer cancel()
|
defer cancel()
|
||||||
@@ -178,5 +183,13 @@ func serveHTTP(ctx context.Context, cfg config, st *store.Store) error {
|
|||||||
if err := srv.ListenAndServe(); err != nil && !errors.Is(err, http.ErrServerClosed) {
|
if err := srv.ListenAndServe(); err != nil && !errors.Is(err, http.ErrServerClosed) {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
// ListenAndServe returns at the stop, before its requests have
|
||||||
|
// (net/http, Server.Shutdown); Shutdown returns once they have. The links
|
||||||
|
// they mail after their answers go on after that, and end before the
|
||||||
|
// database closes.
|
||||||
|
<-shutDown
|
||||||
|
drainCtx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
|
||||||
|
defer cancel()
|
||||||
|
srvApp.Drain(drainCtx)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|||||||
+17
-2
@@ -97,6 +97,12 @@ type config struct {
|
|||||||
// how often build/postfix-cert-reload.sh looks for a renewal.
|
// how often build/postfix-cert-reload.sh looks for a renewal.
|
||||||
tlsServedCertFile string
|
tlsServedCertFile string
|
||||||
tlsReloadInterval time.Duration
|
tlsReloadInterval time.Duration
|
||||||
|
// smtpBanner is Postfix's smtpd_banner, expanded, read from postconf at
|
||||||
|
// the start: Health shows the greeting it makes.
|
||||||
|
smtpBanner string
|
||||||
|
// timeZone is TZ as the container was started with ("" for none): the
|
||||||
|
// zone of every time and day the panel shows, which Settings names.
|
||||||
|
timeZone string
|
||||||
|
|
||||||
dkimDir string
|
dkimDir string
|
||||||
dkimSelectorDef string
|
dkimSelectorDef string
|
||||||
@@ -147,6 +153,7 @@ func loadConfig() config {
|
|||||||
manifestPath: filepath.Join(dataDir, backup.ManifestName),
|
manifestPath: filepath.Join(dataDir, backup.ManifestName),
|
||||||
setupTokenPath: envDefault("SELFPOST_SETUP_TOKEN_FILE", filepath.Join(dataDir, "setup-token")),
|
setupTokenPath: envDefault("SELFPOST_SETUP_TOKEN_FILE", filepath.Join(dataDir, "setup-token")),
|
||||||
hostname: os.Getenv("SELFPOST_HOSTNAME"),
|
hostname: os.Getenv("SELFPOST_HOSTNAME"),
|
||||||
|
timeZone: os.Getenv("TZ"),
|
||||||
// Secure cookies by default (security.md); PANEL_COOKIE_SECURE=false is a
|
// Secure cookies by default (security.md); PANEL_COOKIE_SECURE=false is a
|
||||||
// development-only escape hatch for testing over plain HTTP.
|
// development-only escape hatch for testing over plain HTTP.
|
||||||
cookieSecure: envDefault("PANEL_COOKIE_SECURE", "true") != "false",
|
cookieSecure: envDefault("PANEL_COOKIE_SECURE", "true") != "false",
|
||||||
@@ -311,6 +318,13 @@ func openData(cfg config, resync func(*store.Store) error) (st *store.Store, res
|
|||||||
}
|
}
|
||||||
if restored {
|
if restored {
|
||||||
log.Printf("restore manifest accepted; regenerating mail-path maps from SQLite")
|
log.Printf("restore manifest accepted; regenerating mail-path maps from SQLite")
|
||||||
|
// A link taken after the backup was made would work again from it
|
||||||
|
// (security review, 2026-10-17); a link sent before a restore is
|
||||||
|
// asked for again.
|
||||||
|
if err := st.DeleteAllUserLinks(); err != nil {
|
||||||
|
_ = st.Close()
|
||||||
|
return nil, false, err
|
||||||
|
}
|
||||||
if err := resync(st); err != nil {
|
if err := resync(st); err != nil {
|
||||||
_ = st.Close()
|
_ = st.Close()
|
||||||
return nil, false, err
|
return nil, false, err
|
||||||
@@ -323,8 +337,8 @@ func openData(cfg config, resync func(*store.Store) error) (st *store.Store, res
|
|||||||
return st, restored, nil
|
return st, restored, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// run starts the panel's three roles and blocks until a shutdown signal or the
|
// run starts the panel's roles (the list below) and blocks until a shutdown
|
||||||
// first fatal error from any role. A signal triggers a clean stop of all
|
// signal or the first fatal error from any role. A signal triggers a clean stop of all
|
||||||
// roles; a role error cancels the others and is returned so the process exits
|
// roles; a role error cancels the others and is returned so the process exits
|
||||||
// non-zero (letting supervisord/Docker see the failure — architecture.md §
|
// non-zero (letting supervisord/Docker see the failure — architecture.md §
|
||||||
// Image and processes).
|
// Image and processes).
|
||||||
@@ -333,6 +347,7 @@ func run() error {
|
|||||||
// postfix-config.sh has written main.cf before supervisord started the
|
// postfix-config.sh has written main.cf before supervisord started the
|
||||||
// panel (build/entrypoint.sh), and nothing rewrites it after.
|
// panel (build/entrypoint.sh), and nothing rewrites it after.
|
||||||
cfg.tlsServedCertFile = health.ServedCertPath()
|
cfg.tlsServedCertFile = health.ServedCertPath()
|
||||||
|
cfg.smtpBanner = health.SMTPBanner()
|
||||||
|
|
||||||
ctx, stop := signal.NotifyContext(context.Background(), syscall.SIGINT, syscall.SIGTERM)
|
ctx, stop := signal.NotifyContext(context.Background(), syscall.SIGINT, syscall.SIGTERM)
|
||||||
defer stop()
|
defer stop()
|
||||||
|
|||||||
@@ -360,10 +360,11 @@ func signIn(t *testing.T, h http.Handler) *http.Cookie {
|
|||||||
|
|
||||||
// downloadBackup takes a backup through the panel's own /backup route, the way
|
// downloadBackup takes a backup through the panel's own /backup route, the way
|
||||||
// the operator does. An empty password downloads the plain archive; otherwise
|
// the operator does. An empty password downloads the plain archive; otherwise
|
||||||
// the response is a .spbk envelope, which is decrypted here.
|
// the response is a .spbk envelope, which is decrypted here. The account
|
||||||
|
// password goes with it, as the form asks for it (backup-reauth).
|
||||||
func downloadBackup(t *testing.T, h http.Handler, session *http.Cookie, password string) []byte {
|
func downloadBackup(t *testing.T, h http.Handler, session *http.Cookie, password string) []byte {
|
||||||
t.Helper()
|
t.Helper()
|
||||||
form := url.Values{}
|
form := url.Values{"account_password": {restorePassword}}
|
||||||
if password != "" {
|
if password != "" {
|
||||||
form.Set("encrypt", "1")
|
form.Set("encrypt", "1")
|
||||||
form.Set("password", password)
|
form.Set("password", password)
|
||||||
|
|||||||
@@ -104,19 +104,21 @@ func senderCheckDowntime(markAt, now time.Time, ask func() postfixState) (down t
|
|||||||
return 0, false
|
return 0, false
|
||||||
}
|
}
|
||||||
|
|
||||||
// dayPart is the part of a stretch of downtime that fell on one UTC day.
|
// dayPart is the part of a stretch of downtime that fell on one day of the
|
||||||
|
// container's zone.
|
||||||
type dayPart struct {
|
type dayPart struct {
|
||||||
day time.Time
|
day time.Time
|
||||||
down time.Duration
|
down time.Duration
|
||||||
}
|
}
|
||||||
|
|
||||||
// byDay cuts the time from 'from' to 'to' at the UTC midnights between them,
|
// byDay cuts the time from 'from' to 'to' at the midnights of the container's
|
||||||
// so that a night's outage is not all booked on the morning it ended.
|
// zone between them, so that a night's outage is not all booked on the morning
|
||||||
|
// it ended.
|
||||||
func byDay(from, to time.Time) []dayPart {
|
func byDay(from, to time.Time) []dayPart {
|
||||||
from, to = from.UTC(), to.UTC()
|
from, to = from.Local(), to.Local()
|
||||||
var parts []dayPart
|
var parts []dayPart
|
||||||
for from.Before(to) {
|
for from.Before(to) {
|
||||||
next := time.Date(from.Year(), from.Month(), from.Day(), 0, 0, 0, 0, time.UTC).AddDate(0, 0, 1)
|
next := time.Date(from.Year(), from.Month(), from.Day(), 0, 0, 0, 0, time.Local).AddDate(0, 0, 1)
|
||||||
if next.After(to) {
|
if next.After(to) {
|
||||||
next = to
|
next = to
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -62,3 +62,15 @@ func TestByDay(t *testing.T) {
|
|||||||
t.Errorf("no time: %+v", none)
|
t.Errorf("no time: %+v", none)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// In a container with TZ=Europe/Moscow a day is Moscow's: an outage from
|
||||||
|
// 20:50 to 21:10 UTC crosses its midnight (21:00 UTC).
|
||||||
|
func TestByDayInTheContainersZone(t *testing.T) {
|
||||||
|
inZone(t, "Europe/Moscow")
|
||||||
|
from := time.Date(2026, 10, 12, 20, 50, 0, 0, time.UTC)
|
||||||
|
got := byDay(from, from.Add(20*time.Minute))
|
||||||
|
if len(got) != 2 || got[0].down != 10*time.Minute || got[1].down != 10*time.Minute ||
|
||||||
|
got[1].day.Format("2006-01-02 15:04") != "2026-10-13 00:00" {
|
||||||
|
t.Errorf("parts = %+v, want ten minutes on the 12th and ten from Moscow's midnight", got)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
+31
-9
@@ -5,12 +5,17 @@ import (
|
|||||||
"log"
|
"log"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"strings"
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/mixeme/selfpost/internal/milter"
|
"github.com/mixeme/selfpost/internal/milter"
|
||||||
"github.com/mixeme/selfpost/internal/postfix"
|
"github.com/mixeme/selfpost/internal/postfix"
|
||||||
"github.com/mixeme/selfpost/internal/store"
|
"github.com/mixeme/selfpost/internal/store"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// rewriteRetry is how often the signer tries again to write the selector
|
||||||
|
// table when the write at its start failed.
|
||||||
|
const rewriteRetry = 10 * time.Second
|
||||||
|
|
||||||
// serveSigner runs the DKIM signer (internal/milter/sign.go) on its two
|
// serveSigner runs the DKIM signer (internal/milter/sign.go) on its two
|
||||||
// sockets: signSocket for what applications send on 465/587, signed with the
|
// sockets: signSocket for what applications send on 465/587, signed with the
|
||||||
// key of the domain the login belongs to - the sender_login_maps Postfix and
|
// key of the domain the login belongs to - the sender_login_maps Postfix and
|
||||||
@@ -20,16 +25,33 @@ import (
|
|||||||
// build/postfix-config.sh), so while this role is not serving, mail waits.
|
// build/postfix-config.sh), so while this role is not serving, mail waits.
|
||||||
//
|
//
|
||||||
// The keys and selectors come from the key tree and the selector table under
|
// The keys and selectors come from the key tree and the selector table under
|
||||||
// dkimDir (internal/domain, Keys). Before it serves, the table is made to
|
// dkimDir (internal/domain, Keys). Before it serves, the table is rewritten
|
||||||
// hold the server's own name - and written at all on the first start of
|
// from the registry (WriteSelectors): Postfix waits for these sockets
|
||||||
// 2.8.0, whose table replaced OpenDKIM's: Postfix waits for these sockets
|
// (build/postfix-wrapper.sh), so no message meets a table from before. A
|
||||||
// (build/postfix-wrapper.sh), so no message meets the table before it.
|
// rewrite that fails is tried again every rewriteRetry until it succeeds;
|
||||||
|
// meanwhile what the signer finds no key for it defers, and the panel's
|
||||||
|
// pages - Reload configuration among them - stay up.
|
||||||
func serveSigner(ctx context.Context, cfg config, st *store.Store) error {
|
func serveSigner(ctx context.Context, cfg config, st *store.Store) error {
|
||||||
if rebuilt, err := newMailStack(cfg, st).Domains.EnsureServerSigned(); err != nil {
|
domains := newMailStack(cfg, st).Domains
|
||||||
// Mail waits: what the signer cannot find a key for it defers.
|
if err := domains.WriteSelectors(); err != nil {
|
||||||
log.Printf("dkim-signer: the selector table: %v", err)
|
log.Printf("dkim-signer: the selector table: %v - trying again every %s", err, rewriteRetry)
|
||||||
} else if rebuilt {
|
go func() {
|
||||||
log.Printf("dkim-signer: the selector table was rewritten from the registry")
|
tick := time.NewTicker(rewriteRetry)
|
||||||
|
defer tick.Stop()
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case <-ctx.Done():
|
||||||
|
return
|
||||||
|
case <-tick.C:
|
||||||
|
if err := domains.WriteSelectors(); err != nil {
|
||||||
|
log.Printf("dkim-signer: the selector table: %v", err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
log.Printf("dkim-signer: the selector table is written")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}()
|
||||||
}
|
}
|
||||||
keys := milter.NewSignKeys(filepath.Join(cfg.dkimDir, "selectors"), filepath.Join(cfg.dkimDir, "keys"))
|
keys := milter.NewSignKeys(filepath.Join(cfg.dkimDir, "selectors"), filepath.Join(cfg.dkimDir, "keys"))
|
||||||
bind := milter.NewBindings(postfix.New(cfg.postfixDir).SenderLoginMapsPath())
|
bind := milter.NewBindings(postfix.New(cfg.postfixDir).SenderLoginMapsPath())
|
||||||
|
|||||||
@@ -0,0 +1,28 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
// TestMain runs the tests in UTC, whatever the zone of the machine they run
|
||||||
|
// on: the panel shows times and counts days in time.Local, the container's
|
||||||
|
// zone (TZ), and the fixtures are written in UTC. The tests of another zone
|
||||||
|
// set it themselves (inZone).
|
||||||
|
func TestMain(m *testing.M) {
|
||||||
|
time.Local = time.UTC
|
||||||
|
os.Exit(m.Run())
|
||||||
|
}
|
||||||
|
|
||||||
|
// inZone runs the rest of the test in the zone name, as a container started
|
||||||
|
// with TZ=name; the test must not be parallel.
|
||||||
|
func inZone(t *testing.T, name string) {
|
||||||
|
t.Helper()
|
||||||
|
loc, err := time.LoadLocation(name)
|
||||||
|
if err != nil {
|
||||||
|
t.Skipf("no zone %s here: %v", name, err)
|
||||||
|
}
|
||||||
|
time.Local = loc
|
||||||
|
t.Cleanup(func() { time.Local = time.UTC })
|
||||||
|
}
|
||||||
@@ -60,6 +60,12 @@ PANEL_SESSION_IDLE_DAYS=7
|
|||||||
# your own recursor, e.g. SELFPOST_DNS_RESOLVERS=10.0.0.2:53
|
# your own recursor, e.g. SELFPOST_DNS_RESOLVERS=10.0.0.2:53
|
||||||
# SELFPOST_DNS_RESOLVERS=
|
# SELFPOST_DNS_RESOLVERS=
|
||||||
|
|
||||||
|
# Time zone of the container, an IANA name: every time the panel shows, the
|
||||||
|
# days it counts (today on Health, the days of the statistics, the log
|
||||||
|
# filters) and the stamps Postfix writes in mail.log. Leave unset for UTC;
|
||||||
|
# changing it is a restart. Server > Settings shows the zone in use.
|
||||||
|
# TZ=Europe/Moscow
|
||||||
|
|
||||||
# Comma-separated CIDRs (bare IPs allowed) of reverse proxies allowed to
|
# Comma-separated CIDRs (bare IPs allowed) of reverse proxies allowed to
|
||||||
# supply X-Forwarded-For for login/setup rate-limiting. Leave unset unless
|
# supply X-Forwarded-For for login/setup rate-limiting. Leave unset unless
|
||||||
# you know the exact address of your reverse proxy — trusting the wrong
|
# you know the exact address of your reverse proxy — trusting the wrong
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
# Example host Apache vhost for the SelfPost panel (default scenario). Install
|
# Example host Apache vhost for the SelfPost panel (default scenario). Install
|
||||||
# on the host running Apache — NOT inside the SelfPost container. Requires
|
# on the host running Apache — NOT inside the SelfPost container. Requires
|
||||||
# mod_ssl and mod_proxy/mod_proxy_http enabled (`a2enmod ssl proxy proxy_http`).
|
# mod_ssl, mod_proxy/mod_proxy_http and mod_rewrite enabled (`a2enmod ssl proxy proxy_http rewrite`).
|
||||||
#
|
#
|
||||||
# Certificates: obtain with the Apache certbot plugin against THIS vhost
|
# Certificates: obtain with the Apache certbot plugin against THIS vhost
|
||||||
# (`certbot --apache -d mail.example.com`). Certbot edits this file in place to
|
# (`certbot --apache -d mail.example.com`). Certbot edits this file in place to
|
||||||
|
|||||||
@@ -23,7 +23,7 @@
|
|||||||
|
|
||||||
services:
|
services:
|
||||||
selfpost:
|
selfpost:
|
||||||
image: ghcr.io/mixeme/selfpost:2.7.0
|
image: ghcr.io/mixeme/selfpost:2.14.2
|
||||||
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}"
|
||||||
@@ -56,6 +56,9 @@ services:
|
|||||||
# report what the internet sees rather than what this host's own stub
|
# report what the internet sees rather than what this host's own stub
|
||||||
# resolver synthesises. Empty uses public defaults.
|
# resolver synthesises. Empty uses public defaults.
|
||||||
SELFPOST_DNS_RESOLVERS: "${SELFPOST_DNS_RESOLVERS:-}"
|
SELFPOST_DNS_RESOLVERS: "${SELFPOST_DNS_RESOLVERS:-}"
|
||||||
|
# The zone of every time the panel shows and of mail.log's stamps, e.g.
|
||||||
|
# Europe/Moscow. Empty is UTC.
|
||||||
|
TZ: "${TZ:-}"
|
||||||
# Reverse-proxy CIDRs allowed to supply X-Forwarded-For for login/setup
|
# Reverse-proxy CIDRs allowed to supply X-Forwarded-For for login/setup
|
||||||
# rate-limiting. See docs/guide.md "Environment variables" — wrong value lets a
|
# rate-limiting. See docs/guide.md "Environment variables" — wrong value lets a
|
||||||
# client spoof its rate-limit key.
|
# client spoof its rate-limit key.
|
||||||
|
|||||||
+246
-98
@@ -26,15 +26,17 @@ Managed programs ([build/supervisord.conf](../build/supervisord.conf)):
|
|||||||
|
|
||||||
| Program | User | Priority | Role |
|
| Program | User | Priority | Role |
|
||||||
|---|---|---|---|
|
|---|---|---|---|
|
||||||
| `syslog-relay` | panel | 50 | The container's `/dev/log`: passes what OpenDKIM logs through syslog(3) to the container's log (`panel -syslog-relay`) |
|
| `panel` | panel | 200 | HTTP UI + sender check + DKIM signer + journal-milter + log-tailer + rate-limit recalc + notifier; with the inbound relay on also the inbound journal milter |
|
||||||
| `opendkim` | root → `opendkim` (`UserID` in opendkim.conf) | 100 | DKIM signing milter |
|
| `postfix` | root (wrapper) | 300 | MTA — started only after the milter sockets exist (the sender check's, the DKIM signer's two, the journal milter's and, with the inbound relay on, the inbound journal milter's) |
|
||||||
| `panel` | panel | 200 | HTTP UI + sender check + DKIM signer + journal-milter + log-tailer + rate-limit recalc; with the inbound relay on also the inbound journal milter |
|
|
||||||
| `postfix` | root (wrapper) | 300 | MTA — started only after the milter sockets exist (OpenDKIM's, the sender check's, the DKIM signer's two, the journal milter's and, with the inbound relay on, the inbound journal milter's) |
|
|
||||||
| `postfix-reload` | root | — | On-demand `postfix reload` (autostart off) |
|
| `postfix-reload` | root | — | On-demand `postfix reload` (autostart off) |
|
||||||
| `cert-reload` | root | 400 | Daily: re-copies a renewed TLS certificate for Postfix and reloads it |
|
| `cert-reload` | root | 400 | Daily: re-copies a renewed TLS certificate for Postfix and reloads it |
|
||||||
| `logrotate` | root | 400 | Periodic `mail.log` rotation |
|
| `logrotate` | root | 400 | Periodic `mail.log` rotation |
|
||||||
|
|
||||||
Start order: syslog-relay → OpenDKIM → panel (opens the sender check's, the DKIM signer's and the journal-milter's sockets) → Postfix wrapper
|
Until `2.8.0` two more programs ran before the panel: `opendkim`, the DKIM
|
||||||
|
signing milter, and `syslog-relay`, the container's `/dev/log` for what
|
||||||
|
OpenDKIM logged; the panel signs now (§ DKIM signing).
|
||||||
|
|
||||||
|
Start order: panel (opens the sender check's, the DKIM signer's two and the journal-milter's sockets) → Postfix wrapper
|
||||||
polls unix sockets (timeout `MILTER_WAIT_TIMEOUT`, default 30s) then runs
|
polls unix sockets (timeout `MILTER_WAIT_TIMEOUT`, default 30s) then runs
|
||||||
`postfix start-fg` and stays its parent: on TERM it runs `postfix stop`, so a
|
`postfix start-fg` and stays its parent: on TERM it runs `postfix stop`, so a
|
||||||
stop of the program or of the container stops Postfix as Postfix means it to
|
stop of the program or of the container stops Postfix as Postfix means it to
|
||||||
@@ -55,14 +57,15 @@ new one: the entrypoint clears what the last run left in `/run` (the milter
|
|||||||
sockets, the sender check's alive mark). Besides a stop, supervisord itself
|
sockets, the sender check's alive mark). Besides a stop, supervisord itself
|
||||||
dying, or the entrypoint refusing a setting, nothing else exits the
|
dying, or the entrypoint refusing a setting, nothing else exits the
|
||||||
container. `logrotate`'s FATAL (a config logrotate would ignore),
|
container. `logrotate`'s FATAL (a config logrotate would ignore),
|
||||||
`postfix-reload`'s (reported to the page that asked), `cert-reload`'s and
|
`postfix-reload`'s (reported to the page that asked) and `cert-reload`'s
|
||||||
`syslog-relay`'s stay on Server › Health instead.
|
stay on Server › Health instead.
|
||||||
The panel runs `postfix-reload` through supervisord's XML-RPC interface and
|
The panel runs `postfix-reload` through supervisord's XML-RPC interface and
|
||||||
waits for its exit status (`internal/supervisor`, `RunOnce`); the program
|
waits for its exit status (`internal/supervisor`, `RunOnce`); the program
|
||||||
rests EXITED after a run.
|
rests EXITED after a run.
|
||||||
|
|
||||||
**Liveness:** `GET /healthz` (unauthenticated) returns 200 when opendkim,
|
**Liveness:** `GET /healthz` (unauthenticated) returns 200 when panel - whose
|
||||||
panel, and postfix are RUNNING; Docker `HEALTHCHECK` uses the same probe (its
|
roles include the DKIM signer - and postfix are RUNNING
|
||||||
|
(`internal/health`, `Liveness`); Docker `HEALTHCHECK` uses the same probe (its
|
||||||
90-second start period covers postfix's STARTING).
|
90-second start period covers postfix's STARTING).
|
||||||
|
|
||||||
---
|
---
|
||||||
@@ -73,7 +76,7 @@ panel, and postfix are RUNNING; Docker `HEALTHCHECK` uses the same probe (its
|
|||||||
Client ──TLS+SASL──► Postfix (465 smtps, optional 587 submission)
|
Client ──TLS+SASL──► Postfix (465 smtps, optional 587 submission)
|
||||||
│
|
│
|
||||||
├─► sender check (From: against the login, tempfail on failure)
|
├─► sender check (From: against the login, tempfail on failure)
|
||||||
├─► OpenDKIM milter (sign, tempfail on failure)
|
├─► DKIM signer (the login's domain, tempfail on failure)
|
||||||
├─► journal-milter (send log + L2 rate limits, fail-open)
|
├─► journal-milter (send log + L2 rate limits, fail-open)
|
||||||
└─► outbound MX delivery (port 25 client)
|
└─► outbound MX delivery (port 25 client)
|
||||||
|
|
||||||
@@ -96,7 +99,7 @@ smtp/inet` removes Debian's stock smtpd). Outbound delivery still uses the
|
|||||||
- **587/submission** — only when `SUBMISSION_ENABLE=true`; STARTTLS with
|
- **587/submission** — only when `SUBMISSION_ENABLE=true`; STARTTLS with
|
||||||
`smtpd_tls_security_level=encrypt`.
|
`smtpd_tls_security_level=encrypt`.
|
||||||
- **25/smtp inet** — when `INBOUND_RELAY_ENABLE=true` and/or
|
- **25/smtp inet** — when `INBOUND_RELAY_ENABLE=true` and/or
|
||||||
`DMARC_REPORTS_ENABLE=true`. No SASL, no OpenDKIM, no journal-milter —
|
`DMARC_REPORTS_ENABLE=true`. No SASL, no sender check, no signing, no journal-milter —
|
||||||
`smtpd_milters` is always overridden on this service, because main.cf's
|
`smtpd_milters` is always overridden on this service, because main.cf's
|
||||||
submission chain would otherwise let a signing outage defer relayed mail and
|
submission chain would otherwise let a signing outage defer relayed mail and
|
||||||
would file inbound mail into the outbound send log under the sender's
|
would file inbound mail into the outbound send log under the sender's
|
||||||
@@ -110,7 +113,12 @@ smtp/inet` removes Debian's stock smtpd). Outbound delivery still uses the
|
|||||||
verdict headers (`X-Rspamd-Action`, `X-Spamd-Result`) off it. It writes one
|
verdict headers (`X-Rspamd-Action`, `X-Spamd-Result`) off it. It writes one
|
||||||
`inbound_spam_log` row per recipient at an inbound domain - a DMARC report
|
`inbound_spam_log` row per recipient at an inbound domain - a DMARC report
|
||||||
address is not one - and never refuses anything
|
address is not one - and never refuses anything
|
||||||
([internal/milter/inbound.go](../internal/milter/inbound.go)). The headers
|
([internal/milter/inbound.go](../internal/milter/inbound.go)). The row
|
||||||
|
keeps the message's queue id (`{i}` at end-of-message) and starts
|
||||||
|
`queued` - `held` when the filter had Postfix hold it - and the recipient
|
||||||
|
as Postfix logs it (`recipientAsLogged`); the log tailer puts the
|
||||||
|
upstream's answer on it (§ Log tailer, *The upstream's answer*). Older
|
||||||
|
rows of a queue id Postfix uses again are detached first. The headers
|
||||||
are read only while a filter is attached, and only next to the filter's
|
are read only while a filter is attached, and only next to the filter's
|
||||||
signature: a third header, `X-SelfPost-Filter`, the HMAC-SHA256 of the
|
signature: a third header, `X-SelfPost-Filter`, the HMAC-SHA256 of the
|
||||||
message's queue id under the key in `/data/antispam/filter.key`
|
message's queue id under the key in `/data/antispam/filter.key`
|
||||||
@@ -127,9 +135,10 @@ smtp/inet` removes Debian's stock smtpd). Outbound delivery still uses the
|
|||||||
Refusals come from `mail.log`: this listener logs as `postfix/inbound`
|
Refusals come from `mail.log`: this listener logs as `postfix/inbound`
|
||||||
(`syslog_name`), and the log tailer turns a `reject:` line of
|
(`syslog_name`), and the log tailer turns a `reject:` line of
|
||||||
`postfix/inbound/smtpd` (Postfix's own policy) and a `milter-reject` /
|
`postfix/inbound/smtpd` (Postfix's own policy) and a `milter-reject` /
|
||||||
`milter-hold` / `milter-discard` line of `cleanup` (the filter's verdict,
|
`milter-discard` line of `cleanup` (the filter's verdict, counted only for
|
||||||
counted only for a queue id that listener opened) into a row, dated by the
|
a queue id that listener opened) into a row, dated by the line, when the
|
||||||
line, when the recipient is at an inbound domain
|
recipient is at an inbound domain; a message the filter holds goes on to
|
||||||
|
the inbound journal milter, which writes its row as `held`
|
||||||
([internal/logtail/inbound.go](../internal/logtail/inbound.go)). A line
|
([internal/logtail/inbound.go](../internal/logtail/inbound.go)). A line
|
||||||
names one recipient (the last), so a message refused at end-of-message is
|
names one recipient (the last), so a message refused at end-of-message is
|
||||||
one row whatever its recipient count. The tailer saves its place every five seconds,
|
one row whatever its recipient count. The tailer saves its place every five seconds,
|
||||||
@@ -175,8 +184,9 @@ smtp/inet` removes Debian's stock smtpd). Outbound delivery still uses the
|
|||||||
an empty upstream host are omitted from the maps. Optional
|
an empty upstream host are omitted from the maps. Optional
|
||||||
`INBOUND_ANTISPAM_MILTER` only when inbound relay is enabled (not on
|
`INBOUND_ANTISPAM_MILTER` only when inbound relay is enabled (not on
|
||||||
DMARC-only port 25); it is attached with its own `default_action`,
|
DMARC-only port 25); it is attached with its own `default_action`,
|
||||||
`INBOUND_ANTISPAM_MILTER_ACTION`, which is `accept` (fail-open) unless set
|
`INBOUND_ANTISPAM_MILTER_ACTION`, `accept` (fail-open, the default) or
|
||||||
to `tempfail` (main.cf's `milter_default_action` is `tempfail`).
|
`tempfail`, any other value stopping the start (main.cf's
|
||||||
|
`milter_default_action` is `tempfail`).
|
||||||
**DMARC ingest** (`DMARC_REPORTS_ENABLE=true`): allow-listed
|
**DMARC ingest** (`DMARC_REPORTS_ENABLE=true`): allow-listed
|
||||||
report addresses via `check_recipient_access` on `postfix/dmarc_recipients`;
|
report addresses via `check_recipient_access` on `postfix/dmarc_recipients`;
|
||||||
messages pipe to `dmarc-ingest` (symlink to `panel`). When both flags are
|
messages pipe to `dmarc-ingest` (symlink to `panel`). When both flags are
|
||||||
@@ -209,31 +219,110 @@ smtp/inet` removes Debian's stock smtpd). Outbound delivery still uses the
|
|||||||
and to smtpd's `client=` line in mail.log.
|
and to smtpd's `client=` line in mail.log.
|
||||||
- **Chroot disabled** for all services (DNS/TLS inside container).
|
- **Chroot disabled** for all services (DNS/TLS inside container).
|
||||||
- **TLS certs** — read-only mount at `TLS_CERT_FILE` / `TLS_KEY_FILE`, copied
|
- **TLS certs** — read-only mount at `TLS_CERT_FILE` / `TLS_KEY_FILE`, copied
|
||||||
at start into `/etc/postfix/tls-internal`, owned by root and `0600` as
|
at start into `/etc/postfix/tls-internal`, owned by root, the key `0600`
|
||||||
Postfix's TLS_README advises (Postfix itself reads the key as root and does
|
(the certificate `0644`) as Postfix's TLS_README advises (Postfix itself reads the key as root and does
|
||||||
not check its mode), which the mount cannot give; Postfix reads the copy.
|
not check its mode), which the mount cannot give; Postfix reads the copy.
|
||||||
`cert-reload` compares the mount with the copy daily; when they differ and
|
`cert-reload` compares the mount with the copy daily; when they differ and
|
||||||
the certificate and key belong together it renews the copy (each file
|
the certificate and key belong together it renews the copy (each file
|
||||||
renamed into place) and reloads Postfix.
|
renamed into place) and reloads Postfix.
|
||||||
|
|
||||||
### OpenDKIM
|
### DKIM signing
|
||||||
|
|
||||||
Per-domain keys under `/data/opendkim/keys`; `KeyTable` / `SigningTable` maintained
|
The panel signs outgoing mail itself, in a role of its own, `dkim-signer`
|
||||||
by the panel. Socket `/run/opendkim/opendkim.sock`.
|
([cmd/panel/signer.go](../cmd/panel/signer.go),
|
||||||
|
[internal/milter/sign.go](../internal/milter/sign.go)), since `2.8.0`; until
|
||||||
|
then OpenDKIM did, in a program of its own
|
||||||
|
([plans/panel-dkim-signing.md](plans/panel-dkim-signing.md)).
|
||||||
|
|
||||||
The server's own name (`SELFPOST_HOSTNAME`) has a key and a line in both
|
- **Keys** — `/data/dkim/keys/<name>/<selector>.private`, PKCS#1 PEM, one
|
||||||
tables too, without being a row of the registry
|
for each sending domain and one for the server's own name
|
||||||
(`domain.Service.SetServerName`): the panel's notifications are sent as
|
(`internal/domain`, `Keys`; `DKIM_DIR`, default `/data/dkim`). A key is
|
||||||
`postmaster@` that name and signed with it. No application can use it as an
|
made when a domain is added and an existing one is reused, never
|
||||||
envelope sender - the sender map is built from the registry - and since
|
overwritten: that would invalidate the record already published
|
||||||
`2.4.0` none can have a message signed as the server by writing it into
|
(`EnsureKey`). A domain export carries the key and an import writes it. A
|
||||||
`From:` either: the sender check holds the header against the login (§ Sender
|
deleted domain's key directory is removed once the selector table no
|
||||||
check). A sending domain of the same name shares its key and selector with
|
longer names it; one a deletion had to leave, Reload configuration removes
|
||||||
the server; deleting that domain leaves the server signed under the default
|
(`Service.Resync`).
|
||||||
selector, which is the same key only if that was the domain's. At its start
|
- **The selector table** — `/data/dkim/selectors`, a line `<name> <selector>`
|
||||||
the notifier makes sure the tables have the line and name the selector the
|
for every name the server signs, sorted, written whole from the database
|
||||||
panel shows (`EnsureServerSigned`): tables are otherwise rebuilt only when a
|
and renamed into place (`Keys.Rebuild`). It is rewritten when a domain is
|
||||||
domain is added or deleted.
|
added, deleted or imported, by Server › Health › Reload configuration,
|
||||||
|
after a restore, and at every start of the signer before it serves
|
||||||
|
(`WriteSelectors`): Postfix waits for the signer's sockets, so no message
|
||||||
|
meets a table from before - one a rewrite cut short left, the first start
|
||||||
|
of `2.8.0` without one, or one naming the old selector after a changed
|
||||||
|
`DKIM_SELECTOR_DEFAULT`. A rewrite at start that fails is tried again every
|
||||||
|
10 seconds; meanwhile the signer defers what it finds no key for, and the
|
||||||
|
pages stay up. Each role builds a domain service of its own, so the
|
||||||
|
writers are serialised by a mutex (`tableMu`): a writer that read the
|
||||||
|
registry before another's domain was added cannot write after it.
|
||||||
|
- **The signer reads files, not the database.** The panel's one SQLite
|
||||||
|
connection is shared with the pages, and a long query would hold up mail.
|
||||||
|
It reads the table for every message (a few lines; a stamp of time and
|
||||||
|
size would miss a rewrite of the same size within one tick of the file
|
||||||
|
system's clock) and a key again when its file's time or size changes; a key
|
||||||
|
the table no longer names is forgotten. Nothing is reloaded.
|
||||||
|
- **The panel's alone.** All of `/data/dkim` is `panel:panel`, directories
|
||||||
|
`0700`, files `0600`, set by the entrypoint at every start: no group, no
|
||||||
|
other program reads it.
|
||||||
|
- **Two sockets, both strict** (`default_action=tempfail`, so while the role
|
||||||
|
does not answer mail waits, never goes unsigned):
|
||||||
|
`DKIM_SIGN_SOCKET` (`/run/selfpost/sign.sock`) for what applications send
|
||||||
|
on 465/587, signed with the key of the domain the SASL login belongs to -
|
||||||
|
`{auth_authen}`, then `Bindings.DomainOf`, the `sender_login_maps` file
|
||||||
|
Postfix and the sender check read; `DKIM_SIGN_LOCAL_SOCKET`
|
||||||
|
(`/run/selfpost/sign-local.sock`) for what the panel itself submits with
|
||||||
|
`sendmail(1)` (`non_smtpd_milters`), signed with the key of the server's
|
||||||
|
name. The socket a message comes through, not the absence of a login,
|
||||||
|
decides that the server's key may sign it.
|
||||||
|
- **The server's own name** (`SELFPOST_HOSTNAME`) has a key and a line in the
|
||||||
|
table without being a row of the registry (`domain.Service.SetServerName`):
|
||||||
|
the panel's notifications are sent as `postmaster@` that name and signed
|
||||||
|
with it. The name is kept in lower case for DKIM, as the signer finds keys
|
||||||
|
by the name in lower case; the SASL realm, which defaults to the hostname,
|
||||||
|
stays as written. No application can use it as an envelope sender - the
|
||||||
|
sender map is built from the registry - and since `2.4.0` none can have a
|
||||||
|
message signed as the server by writing it into `From:` either: the sender
|
||||||
|
check holds the header against the login (§ Sender check). A sending
|
||||||
|
domain of the same name shares its key and selector with the server;
|
||||||
|
deleting that domain leaves the server signed under the default selector,
|
||||||
|
which is the same key only if that was the domain's.
|
||||||
|
- **What is signed** — `relaxed/simple`, `rsa-sha256`, with
|
||||||
|
`github.com/emersion/go-msgauth` v0.7.0 (`dkim`). `h=`: every header of
|
||||||
|
the message whose name is in RFC 6376 § 5.4.1's list as OpenDKIM's
|
||||||
|
`dkim_should_signhdrs[]` held it, and `Sender` and `List-Unsubscribe-Post`
|
||||||
|
besides (`signHeaders`), each occurrence, in message order; then, once
|
||||||
|
more, each of `Subject`, `Date`, `To`, `Cc`, `Reply-To` and `Sender` the
|
||||||
|
message has (`overSign` - only those present: Postfix's cleanup adds a
|
||||||
|
missing `Date:` after the milters, out of their sight, and a name in `h=`
|
||||||
|
the message lacked would break the signature over the one added); then `From` once more, last (`signLines`). Each header is hashed as
|
||||||
|
Postfix will deliver it: the value with its leading space (the signer asks
|
||||||
|
for `SMFIP_HDR_LEADSPC`), the lines of a folded one joined with CRLF. The
|
||||||
|
signature is inserted at the top, above Postfix's own `Received:`, as
|
||||||
|
`DKIM-Signature: ...`. `disable_mime_output_conversion=yes`
|
||||||
|
([build/postfix-config.sh](../build/postfix-config.sh)): an 8-bit message
|
||||||
|
is delivered as it was signed, not converted to 7-bit for a server that
|
||||||
|
does not announce 8BITMIME.
|
||||||
|
- **Limits.** What cannot be signed - no line in the table, no key file, a
|
||||||
|
login with no domain, a read error - is deferred. A header section over
|
||||||
|
1 MiB is refused, `552 5.3.4`: the signer holds it whole until the body
|
||||||
|
starts. A body line over 998 bytes is logged (`dkim-signer: <domain> ...`)
|
||||||
|
and the message goes on: Postfix's SMTP client breaks such a line on
|
||||||
|
delivery (`smtp_line_length_limit`), and the signature will not verify. A
|
||||||
|
message has an hour from the end of its headers to its end - go-milter
|
||||||
|
v0.4.1 tells a session nothing of a dropped connection - after which it is
|
||||||
|
deferred, never signed over part of its body.
|
||||||
|
- **Logs** go to the panel's stderr, the container's log, as
|
||||||
|
`panel: … dkim-signer:` lines; nothing is written to `mail.log` (nor did OpenDKIM write there).
|
||||||
|
- **The old tree,** `/data/opendkim` - the keys, `KeyTable` and
|
||||||
|
`SigningTable` until `2.8.0`, shared with OpenDKIM through group
|
||||||
|
`selfpost`. `2.8.x` copied its keys to `/data/dkim/keys` at its first
|
||||||
|
start and kept it for a rollback to a 2.7.x image; since `2.9.0` the
|
||||||
|
entrypoint removes it once `/data/dkim` holds a key, and a `/data` with
|
||||||
|
keys under the old tree only - one that never ran 2.8.x - stops the start
|
||||||
|
(the server's name would otherwise get a new key and its published record
|
||||||
|
would no longer match). Under 2.8.x it was the panel's alone, like the
|
||||||
|
new one, and left out of backups.
|
||||||
|
|
||||||
### Panel binary ([cmd/panel](../cmd/panel))
|
### Panel binary ([cmd/panel](../cmd/panel))
|
||||||
|
|
||||||
@@ -261,9 +350,8 @@ milter, § Postfix above, port 25):
|
|||||||
with the key of the domain the SASL login belongs to) and
|
with the key of the domain the SASL login belongs to) and
|
||||||
`DKIM_SIGN_LOCAL_SOCKET` (the panel's own mail, `non_smtpd_milters`,
|
`DKIM_SIGN_LOCAL_SOCKET` (the panel's own mail, `non_smtpd_milters`,
|
||||||
signed with the key of the server's name); after the sender check and
|
signed with the key of the server's name); after the sender check and
|
||||||
**strict**. `relaxed/simple`, RFC 6376 § 5.4.1's headers and `From` once
|
**strict**. Keys and the selector table from `/data/dkim`, no database:
|
||||||
more, go-msgauth/dkim; keys and selectors from OpenDKIM's KeyTable while
|
§ DKIM signing above.
|
||||||
both sign (`internal/milter/sign.go`).
|
|
||||||
4. **journal-milter** — unix socket `JOURNAL_MILTER_SOCKET`; records From/To/
|
4. **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
|
||||||
@@ -287,12 +375,13 @@ milter, § Postfix above, port 25):
|
|||||||
7. **notifier** — writes to the panel's users when something needs them
|
7. **notifier** — writes to the panel's users when something needs them
|
||||||
(§ Notifications below).
|
(§ Notifications below).
|
||||||
|
|
||||||
Milter chain in Postfix: the sender check (tempfail), the panel's DKIM
|
Milter chain in Postfix ([build/postfix-config.sh](../build/postfix-config.sh),
|
||||||
signer (tempfail), OpenDKIM (tempfail), then the journal (accept on
|
`smtpd_milters`): the sender check (tempfail), the DKIM signer (tempfail),
|
||||||
failure); until OpenDKIM leaves the image both sign every message
|
then the journal (accept on failure) - the check first, since the signature
|
||||||
(docs/plans/panel-dkim-signing.md, stage 1). Mail submitted locally with
|
is held to the `From:` domain and a message the check could not judge must
|
||||||
`sendmail(1)` — the notifier's — passes the signer's own socket and OpenDKIM
|
not be signed. Until `2.8.0` OpenDKIM signed in the signer's place. Mail
|
||||||
(`non_smtpd_milters`): it is signed, it is the panel's own and so needs no
|
submitted locally with `sendmail(1)` — the notifier's — passes the signer's
|
||||||
|
own socket alone (`non_smtpd_milters`, tempfail): it is signed, it is the panel's own and so needs no
|
||||||
check of whose it is, and it is not an application's mail to be journalled
|
check of whose it is, and it is not an application's mail to be journalled
|
||||||
or limited. Bounces Postfix
|
or limited. Bounces Postfix
|
||||||
generates pass no milter.
|
generates pass no milter.
|
||||||
@@ -302,10 +391,13 @@ generates pass no milter.
|
|||||||
[internal/milter/sender.go](../internal/milter/sender.go) holds a message's
|
[internal/milter/sender.go](../internal/milter/sender.go) holds a message's
|
||||||
`From:` header against the application that sends it
|
`From:` header against the application that sends it
|
||||||
([plans/from-header-binding.md](plans/from-header-binding.md)). Postfix holds
|
([plans/from-header-binding.md](plans/from-header-binding.md)). Postfix holds
|
||||||
the envelope sender against the SASL login, and OpenDKIM chooses the signing
|
the envelope sender against the SASL login, and the signer signs with the key
|
||||||
key by the header; until `2.4.0` nothing connected the two, and an
|
of the login's domain (§ DKIM signing): the signature's `d=` is that domain,
|
||||||
application could have mail signed as another domain of the server or as the
|
and a receiver holds it to the domain of `From:` (DMARC, RFC 7489 § 3.1.1).
|
||||||
server itself.
|
Without the check an application could send under the server's signature as
|
||||||
|
another domain of the server or as the server itself. The check came in
|
||||||
|
`2.4.0`, when OpenDKIM still signed and chose its key by the header itself;
|
||||||
|
the hole it closed was the same.
|
||||||
|
|
||||||
- **The rule is the envelope's.** The check reads the file Postfix reads for
|
- **The rule is the envelope's.** The check reads the file Postfix reads for
|
||||||
`smtpd_sender_login_maps` and makes the same lookup: the address itself is
|
`smtpd_sender_login_maps` and makes the same lookup: the address itself is
|
||||||
@@ -324,20 +416,24 @@ server itself.
|
|||||||
`src/global/mail_addr_find.c`, `mail_addr_find_opt()`). A key written
|
`src/global/mail_addr_find.c`, `mail_addr_find_opt()`). A key written
|
||||||
twice makes Postfix refuse the whole table, and the check defers then too.
|
twice makes Postfix refuse the whole table, and the check defers then too.
|
||||||
- **How the header is read.** Exactly one `From:` field (none, or more than
|
- **How the header is read.** Exactly one `From:` field (none, or more than
|
||||||
one, is refused), read with OpenDKIM's own parser: libopendkim's
|
one, is refused). The line, its folds joined, must be one address in RFC
|
||||||
`dkim_mail_parse()`, ported line for line from Debian's opendkim
|
5322's grammar as Go's `net/mail` reads it - a mail program's reading, the
|
||||||
2.11.0~beta2 (`internal/milter/mailparse.go`) and tested against the C on
|
address it shows; a display name may say anything (an encoded one in a
|
||||||
two hundred thousand lines. The check asks Postfix for header values with
|
charset Go does not convert is passed through unread). A line a lenient
|
||||||
their leading space, as OpenDKIM does (`SMFIP_HDR_LEADSPC`), so both read the
|
program could show as another address - `ceo@bank.com <me@mine.com>`,
|
||||||
same bytes. The address it returns is the one judged - the one OpenDKIM
|
`<b@evil.com, a@mine.com>` - is refused. That address, its local part
|
||||||
signs for - whatever a display name, a comment or a quoted local part
|
unquoted as Postfix holds the envelope's, is what the bindings are asked
|
||||||
holds; a parse failure, no address, or a second one after it is refused,
|
about, folded to lower case in ASCII only: a Unicode fold would make the
|
||||||
and so is an address over 256 bytes, which OpenDKIM would match its
|
Kelvin sign a `k` and let a look-alike pass. Since `2.8.0` a `Sender:`
|
||||||
`SigningTable` against cut short (`dkimf_apply_signtable()`,
|
line - the sending agent's, shown beside `From:` as "on behalf of" - is
|
||||||
`MAXADDRESS`). Encoded words are not decoded, by either. The line must also
|
held to the same rule when the message has one (at most one; the owner's
|
||||||
be one address in RFC 5322's grammar (`net/mail`), at the domain OpenDKIM
|
decision, 2026-10-16): the reply says `Sender:` instead of `From:` and the
|
||||||
signs for, so that a mail program shows the address that is signed; that
|
row's reason is `sender`. `From:` is judged first. The check asks Postfix for
|
||||||
address, its local part unquoted, is what the bindings are asked about.
|
header values with their leading space (`SMFIP_HDR_LEADSPC`), as the
|
||||||
|
signer does, so both read the same bytes. Until `2.8.0` the line was also
|
||||||
|
read with a port of OpenDKIM's own parser, so that the address judged was
|
||||||
|
the one OpenDKIM signed for, and an address over 256 bytes was refused;
|
||||||
|
both went with OpenDKIM.
|
||||||
- **The verdict** comes at the end of the headers: `550 5.7.1`, permanent,
|
- **The verdict** comes at the end of the headers: `550 5.7.1`, permanent,
|
||||||
naming the line, as a reply code of the milter's own. What the client wrote
|
naming the line, as a reply code of the milter's own. What the client wrote
|
||||||
is reduced to printable ASCII and cut before it is said back, logged or
|
is reduced to printable ASCII and cut before it is said back, logged or
|
||||||
@@ -381,10 +477,10 @@ server itself.
|
|||||||
cleanup process serves message after message, so each such warning is
|
cleanup process serves message after message, so each such warning is
|
||||||
counted as a message. The two are counted apart (the owner, 2026-10-14):
|
counted as a message. The two are counted apart (the owner, 2026-10-14):
|
||||||
connections turned away in `deferred`, messages deferred mid-message in
|
connections turned away in `deferred`, messages deferred mid-message in
|
||||||
`messages` of `sender_check_days`, a row per UTC day kept a year, and
|
`messages` of `sender_check_days`, a row per day of the container's zone (`TZ`) kept a year, and
|
||||||
Server › Health shows today, 30 days and 12 months in a column each. The
|
Server › Health shows today, 30 days and 12 months in a column each. The
|
||||||
counts also see a panel that is running and not answering, which the mark
|
counts also see a panel that is running and not answering, which the mark
|
||||||
does not. Downtime is cut at the UTC midnights it crossed. A log read again
|
does not. Downtime is cut at the midnights of that zone it crossed. A log read again
|
||||||
after an unclean stop does not count again: the count and the place in the
|
after an unclean stop does not count again: the count and the place in the
|
||||||
log it reached (the file's fingerprint and the offset past the line) are
|
log it reached (the file's fingerprint and the offset past the line) are
|
||||||
saved in one transaction, and while the tailer catches up a warning at or
|
saved in one transaction, and while the tailer catches up a warning at or
|
||||||
@@ -480,9 +576,12 @@ and `MAIL_LOG` in [cmd/panel/main.go](../cmd/panel/main.go). Backups exclude
|
|||||||
Rotation uses rename + `postfix reload`
|
Rotation uses rename + `postfix reload`
|
||||||
([build/logrotate-mail.conf](../build/logrotate-mail.conf)), not `copytruncate` —
|
([build/logrotate-mail.conf](../build/logrotate-mail.conf)), not `copytruncate` —
|
||||||
the latter can drop `status=sent` lines and leave send-log rows stuck at
|
the latter can drop `status=sent` lines and leave send-log rows stuck at
|
||||||
`queued`. After rename, logrotate runs `create 0640 postfix selfpost` (postlogd
|
`queued`. After rename, logrotate runs `create 0640 postfix selfpost`, so the
|
||||||
recreates the file on its first line after the reload, as root and mode
|
file is there, readable by the panel through group `selfpost`, before postlogd
|
||||||
`0600`, which the unprivileged panel user cannot read). `follow()` drains the
|
writes its first line after the reload; a file postlogd makes itself (one
|
||||||
|
removed while the container runs) is `0640` too, through
|
||||||
|
`maillog_file_permissions=0640` - Postfix's default is `0600`, which the
|
||||||
|
unprivileged panel user could not read. `follow()` drains the
|
||||||
old inode once more before switching descriptors, and keeps it open and reads
|
old inode once more before switching descriptors, and keeps it open and reads
|
||||||
it for a minute after: postlogd keeps writing to the renamed file until the
|
it for a minute after: postlogd keeps writing to the renamed file until the
|
||||||
reload makes it exit, seconds later (Postfix 3.10.13, src/postlogd/postlogd.c;
|
reload makes it exit, seconds later (Postfix 3.10.13, src/postlogd/postlogd.c;
|
||||||
@@ -496,10 +595,44 @@ inbound journal's. The tail of a line is the far end's words (a reply, a HELO
|
|||||||
name), and a pattern that could start there let a stranger's HELO name file a
|
name), and a pattern that could start there let a stranger's HELO name file a
|
||||||
delivery (audit 2026-10-12, M12).
|
delivery (audit 2026-10-12, M12).
|
||||||
|
|
||||||
|
**The test message** (test-email, `2.12.0`) is the one row the panel writes
|
||||||
|
itself: Server › Health hands it to `sendmail(1)`, which tells nothing of the
|
||||||
|
queue id, and the journal-milter does not see local mail. The row is written
|
||||||
|
first, queued and with no queue id, and its Message-ID is awaited
|
||||||
|
(`logtail.AwaitMessageID`, in memory, ten minutes); cleanup's line
|
||||||
|
`<id>: message-id=<…>` (Postfix 3.10.13, `src/cleanup/cleanup_message.c:639`)
|
||||||
|
gives the row its queue id, and from then on its delivery lines update it as
|
||||||
|
any row's. A restart in the second between leaves it without one, and the
|
||||||
|
reconcile sweep closes it once the id is no longer awaited
|
||||||
|
(`CloseUnmatchedTestMessages`); an id awaited longer than that is not taken
|
||||||
|
any more. sendmail out of time keeps the row and the id - postdrop may have
|
||||||
|
queued the message all the same.
|
||||||
|
|
||||||
|
**The upstream's answer** on the inbound journal (inbound-upstream-result,
|
||||||
|
`2.10.0`). Mail accepted on port 25 is handed to its upstream by the same
|
||||||
|
`smtp` client as outbound mail, and its delivery lines look alike; what tells
|
||||||
|
them apart is the row. A delivery line no send-log row takes (`UpdateStatus`
|
||||||
|
changed nothing) is offered to the journal: the rows of its queue id and
|
||||||
|
recipient take the status, the host (`relay=`, `""` for `none` - a line
|
||||||
|
written without a session, Postfix 3.10.13, `src/smtp/smtp_trouble.c:273,
|
||||||
|
421`), the reply - the `(%s)` after the first `status=`, made printable and
|
||||||
|
cut to 255 bytes - and the line's time (`store.UpdateInboundUpstream`). Only
|
||||||
|
a `queued`, `held` or `deferred` row moves: `sent` and `bounced` are final, so
|
||||||
|
a line read twice after a restart changes nothing. The addresses are read by
|
||||||
|
Postfix's quoting (`loggedAddress`), so a quoted local part cannot end one
|
||||||
|
early. cleanup's `removed (canceled)` - a queue file never queued - detaches
|
||||||
|
the rows the milter had written for it. qmgr's expiry line, which names no
|
||||||
|
recipient, makes the rows of its queue id `bounced`, keeping the last reply
|
||||||
|
after "expired in the queue" (`ExpireInboundUpstream`). It is an update per
|
||||||
|
attempt, never a row: the journal's caps stand, and port-25 mail's lines are
|
||||||
|
still not stored (delivery-log-storage). A bounce Postfix sends to a stranger
|
||||||
|
is mail of its own queue id and matches no row.
|
||||||
|
|
||||||
**Read offset is persisted** (`logtail_state` table, in the baseline schema): the
|
**Read offset is persisted** (`logtail_state` table, in the baseline schema): the
|
||||||
tailer stores its position plus a fingerprint of the log - the hash of its first
|
tailer stores its position plus a fingerprint of the log - the hash of its first
|
||||||
512 bytes and the file's device and inode, so two logs that begin alike are two
|
512 bytes and the file's inode (not the device, which can change across a
|
||||||
files and a renamed file keeps its fingerprint - and on start resumes from it,
|
host's reboot), so two logs that begin alike are two files and a renamed file
|
||||||
|
keeps its fingerprint - and on start resumes from it,
|
||||||
parsing the tail written while the panel was down. If the fingerprint no longer
|
parsing the tail written while the panel was down. If the fingerprint no longer
|
||||||
matches, the log was rotated in the meantime: the tailer first opens
|
matches, the log was rotated in the meantime: the tailer first opens
|
||||||
`mail.log.1` (left plain by `delaycompress`), and if that is the file the saved
|
`mail.log.1` (left plain by `delaycompress`), and if that is the file the saved
|
||||||
@@ -508,7 +641,7 @@ start - what Postfix wrote between the stop and the rotation is not lost. A
|
|||||||
`mail.log.1` that is another file (rotated twice) is logged with the time from
|
`mail.log.1` that is another file (rotated twice) is logged with the time from
|
||||||
which nothing was read. Re-parsing lines is harmless because `UpdateStatus`
|
which nothing was read. Re-parsing lines is harmless because `UpdateStatus`
|
||||||
writes the same status onto the same row. Only a first-ever start, with nothing
|
writes the same status onto the same row. Only a first-ever start, with nothing
|
||||||
stored, begins at end-of-file, so installing the panel does not replay a
|
stored (or a stored place that cannot be read), begins at end-of-file, so installing the panel does not replay a
|
||||||
pre-existing log.
|
pre-existing log.
|
||||||
|
|
||||||
**A refusal's line, and a refusal the panel did not write.** A message refused
|
**A refusal's line, and a refusal the panel did not write.** A message refused
|
||||||
@@ -573,7 +706,11 @@ folding case). A delivery line names it in `orig_to=` when Postfix rewrote
|
|||||||
the address, in `to=` otherwise (`log_adhoc.c` writes `orig_to=` only then),
|
the address, in `to=` otherwise (`log_adhoc.c` writes `orig_to=` only then),
|
||||||
and is matched without regard to case. It updates only the newest row of its
|
and is matched without regard to case. It updates only the newest row of its
|
||||||
queue id and recipient: Postfix reuses a short queue id once the message has
|
queue id and recipient: Postfix reuses a short queue id once the message has
|
||||||
left the queue, and an old row must not be rewritten. `UpdateStatus`'s transaction
|
left the queue, and an old row must not be rewritten. Since `2.10.1` the
|
||||||
|
image makes long, non-repeating queue ids (`enable_long_queue_ids=yes`,
|
||||||
|
[build/postfix-config.sh](../build/postfix-config.sh)): the time in seconds,
|
||||||
|
then the microseconds, `z` and the inode; a queue file from before keeps its
|
||||||
|
short name, and every pattern of the panel takes both. `UpdateStatus`'s transaction
|
||||||
starts with a write, so a commit by the `dmarc-ingest` process in between
|
starts with a write, so a commit by the `dmarc-ingest` process in between
|
||||||
cannot fail it with `database is locked`.
|
cannot fail it with `database is locked`.
|
||||||
Now that the log survives the container, reaching this path should mean the
|
Now that the log survives the container, reaching this path should mean the
|
||||||
@@ -581,7 +718,7 @@ lines are gone for good — rotated past fourteen files while the panel was down
|
|||||||
or deleted — or that the tailer was not reading.
|
or deleted — or that the tailer was not reading.
|
||||||
|
|
||||||
Whether the sweep earns its place is measured, not argued (audit 2026-10-12,
|
Whether the sweep earns its place is measured, not argued (audit 2026-10-12,
|
||||||
H3). `delivery_log_days` (migration `0007`) keeps, per UTC day for a year:
|
H3). `delivery_log_days` (migration `0007`) keeps, per day of the container's zone for a year:
|
||||||
`reconcile_closed`, the rows the sweep closed; `reconcile_missed`, those of
|
`reconcile_closed`, the rows the sweep closed; `reconcile_missed`, those of
|
||||||
them whose final line - sent, bounced, or the message's expiry - was in
|
them whose final line - sent, bounced, or the message's expiry - was in
|
||||||
`mail.log` or `mail.log.1` when the sweep closed them - the tailer did not
|
`mail.log` or `mail.log.1` when the sweep closed them - the tailer did not
|
||||||
@@ -684,7 +821,9 @@ building them only takes method values, so the route guard test can run it on
|
|||||||
a bare `Server` and ask which pattern answers a path:
|
a bare `Server` and ask which pattern answers a path:
|
||||||
|
|
||||||
- **public** — `/healthz` (liveness), `/license` (the embedded `LICENSE`),
|
- **public** — `/healthz` (liveness), `/license` (the embedded `LICENSE`),
|
||||||
`/static/`, `/setup/`, `/login`, `/logout`;
|
`/static/`, `/setup/`, `/login`, `/logout`, and the links of
|
||||||
|
password-reset, opened from a mail signed in or not: `/login/reset`,
|
||||||
|
`/login/reset/<token>`, `/confirm/<token>` (`internal/web/auth/links.go`);
|
||||||
- **authenticated** — everything else, behind `RequireAuth`: a request without
|
- **authenticated** — everything else, behind `RequireAuth`: a request without
|
||||||
a live session is redirected to `/login`.
|
a live session is redirected to `/login`.
|
||||||
|
|
||||||
@@ -707,8 +846,8 @@ only reads (the delete confirmation at `GET …/delete` renders a form).
|
|||||||
| Outbound › DMARC reports | `GET /outbound/dmarc` · `…/dmarc/domains/{id}` · `…/dmarc/reports/{id}` — registered only with `DMARC_REPORTS_ENABLE=true` |
|
| Outbound › DMARC reports | `GET /outbound/dmarc` · `…/dmarc/domains/{id}` · `…/dmarc/reports/{id}` — registered only with `DMARC_REPORTS_ENABLE=true` |
|
||||||
| Inbound › Domains | `GET, POST /inbound/domains` · `/inbound/domains/{id}` · `POST …/dns-recheck`, `…/upstream`, `…/recipients` · `GET, POST …/delete` — registered only with `INBOUND_RELAY_ENABLE=true` |
|
| Inbound › Domains | `GET, POST /inbound/domains` · `/inbound/domains/{id}` · `POST …/dns-recheck`, `…/upstream`, `…/recipients` · `GET, POST …/delete` — registered only with `INBOUND_RELAY_ENABLE=true` |
|
||||||
| Inbound › Log, Filter lists | `GET /inbound/log` · `GET, POST /inbound/filter-lists` · `POST /inbound/filter-lists/{id}/delete` — registered only with `INBOUND_RELAY_ENABLE=true` |
|
| Inbound › Log, Filter lists | `GET /inbound/log` · `GET, POST /inbound/filter-lists` · `POST /inbound/filter-lists/{id}/delete` — registered only with `INBOUND_RELAY_ENABLE=true` |
|
||||||
| Server | `GET /server/health` (+ `/fragment`) · `POST /server/health/recheck`, `/server/health/reload` · `GET /server/log` (+ `/fragment`) · `GET, POST /server/backup` · `POST /server/backup/import` · `/server/users`, `…/new`, `…/{uid}`, `…/{uid}/delete` · `GET, POST /server/settings` · `GET /server/components` |
|
| Server | `GET /server/health` (+ `/fragment`) · `POST /server/health/recheck`, `/server/health/reload`, `/server/health/test-message` · `GET /server/log` (+ `/fragment`) · `GET, POST /server/backup` · `POST /server/backup/import` · `/server/users`, `…/new`, `…/{uid}`, `…/{uid}/delete` · `GET, POST /server/settings` · `POST /server/settings/dns-recheck` · `GET /server/components` |
|
||||||
| Account | `GET /account` · `POST /account/profile`, `/account/password` |
|
| Account | `GET /account` · `POST /account/profile`, `/account/password`, `/account/email/confirm` |
|
||||||
| User menu | `GET /help` · `POST /logout` (public mux) |
|
| User menu | `GET /help` · `POST /logout` (public mux) |
|
||||||
|
|
||||||
An application is addressed under its domain, and an application id that does
|
An application is addressed under its domain, and an application id that does
|
||||||
@@ -809,9 +948,8 @@ is one layout and one way to render.
|
|||||||
package (`NewOutDomain`, `NewHelp`, …) that the handler fills. The shell is
|
package (`NewOutDomain`, `NewHelp`, …) that the handler fills. The shell is
|
||||||
derived from `Meta` and the feature flags (`SetInboundEnabled`,
|
derived from `Meta` and the feature flags (`SetInboundEnabled`,
|
||||||
`SetDMARCEnabled`) in one place, `Engine.shell`, so which entries the menu
|
`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
|
has is decided once. No page's data is a map: the health sampler too
|
||||||
in the handlers is the health sampler's, which is turned into a typed
|
returns a typed `health.Machine`.
|
||||||
`Health` before rendering.
|
|
||||||
- **The component kit.** [components.html](../internal/web/view/templates/components.html)
|
- **The component kit.** [components.html](../internal/web/view/templates/components.html)
|
||||||
holds one `{{define}}` per partial (page head, postmark, box and its head and
|
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,
|
foot, flash, health card, DNS record, facts, side menu, timeline, log pane,
|
||||||
@@ -851,8 +989,8 @@ has the component skeleton of its mockup), `TestRedesignedPagesLoadOnlyTheKit`,
|
|||||||
ratchet list `internal/web/view/legacy_pages.txt` is empty and can only shrink;
|
ratchet list `internal/web/view/legacy_pages.txt` is empty and can only shrink;
|
||||||
the CI step
|
the CI step
|
||||||
[design-first](../.github/scripts/design-first.sh) rejects a commit that
|
[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
|
changes a guard together with the code it judges, and a template that is
|
||||||
byte-for-byte return of one from history. The design contract and the process
|
the same as one from history, whitespace aside. The design contract and the process
|
||||||
are in [plans/panel-redesign.md](plans/panel-redesign.md) § Enforcement.
|
are in [plans/panel-redesign.md](plans/panel-redesign.md) § Enforcement.
|
||||||
|
|
||||||
### Security headers, CSP and scripts
|
### Security headers, CSP and scripts
|
||||||
@@ -876,7 +1014,8 @@ send it (an older one, or a current one over plain HTTP at a name other than
|
|||||||
`localhost`), `Origin` must name the panel's host - which a form of the panel
|
`localhost`), `Origin` must name the panel's host - which a form of the panel
|
||||||
carries under its `Referrer-Policy: same-origin`, the page's own origin (under
|
carries under its `Referrer-Policy: same-origin`, the page's own origin (under
|
||||||
`no-referrer` it would be `null` and refused); a request with neither header
|
`no-referrer` it would be `null` and refused); a request with neither header
|
||||||
is let through (an accepted risk). A refusal logs the `Sec-Fetch-Site`,
|
is refused too (origin-strict, `2.10.4`; it was let through before, as an
|
||||||
|
accepted risk). A refusal logs the `Sec-Fetch-Site`,
|
||||||
`Origin` and `Host` it compared.
|
`Origin` and `Host` it compared.
|
||||||
There are no CSRF tokens — see [security.md](security.md).
|
There are no CSRF tokens — see [security.md](security.md).
|
||||||
|
|
||||||
@@ -925,19 +1064,19 @@ the cookie and the restored row's `expires_at` has not passed.
|
|||||||
## Code layers
|
## Code layers
|
||||||
|
|
||||||
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, DKIM key or selector 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, users,
|
`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, the milter protocol, DNS or the log file, which is what makes them
|
||||||
substitutable in tests — `milter.Store`, `app.SenderMaps` and
|
substitutable in tests — `milter.Store`, `app.SenderMaps` and
|
||||||
`logtail.StatusStore` are the seams the unit tests replace with fakes.
|
`logtail.StatusStore` are the seams the unit tests replace with fakes.
|
||||||
|
|
||||||
```mermaid
|
```mermaid
|
||||||
flowchart TB
|
flowchart TB
|
||||||
subgraph cmd ["cmd — composition root"]
|
subgraph cmd ["cmd — composition root"]
|
||||||
panel["panel: HTTP + journal-milter + log-tailer"]
|
panel["panel: HTTP + sender check + DKIM signer + journal-milter + log-tailer"]
|
||||||
backupcli["selfpost-backup CLI"]
|
backupcli["selfpost-backup CLI"]
|
||||||
end
|
end
|
||||||
subgraph web ["internal/web — HTTP surface"]
|
subgraph web ["internal/web — HTTP surface"]
|
||||||
@@ -995,7 +1134,7 @@ flowchart TB
|
|||||||
dmarcSvc --> postfix
|
dmarcSvc --> postfix
|
||||||
```
|
```
|
||||||
|
|
||||||
The roles inside the `panel` process (HTTP server, sender check,
|
The roles inside the `panel` process (HTTP server, sender check, DKIM signer,
|
||||||
journal-milter, log-tailer, rate-limit recalc, notifier) share one binary and
|
journal-milter, log-tailer, rate-limit recalc, notifier) share one binary and
|
||||||
one SQLite handle on purpose — see
|
one SQLite handle on purpose — see
|
||||||
[Panel binary](#panel-binary-cmdpanel) for why, and *Persistence* below for the
|
[Panel binary](#panel-binary-cmdpanel) for why, and *Persistence* below for the
|
||||||
@@ -1009,7 +1148,7 @@ single-connection trade-off that follows from it.
|
|||||||
|---|---|
|
|---|---|
|
||||||
| `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) |
|
| `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 |
|
| `dkim/` | DKIM keys (`keys/<name>/<selector>.private`) and the selector table (`selectors`) the signer reads; the panel's alone, `0700` / `0600` — § DKIM signing |
|
||||||
| `sasl/sasldb2` | Application SASL credentials |
|
| `sasl/sasldb2` | Application SASL credentials |
|
||||||
| `postfix/sender_login_maps` | Login → From binding |
|
| `postfix/sender_login_maps` | Login → From binding |
|
||||||
| `postfix/relay_domains` | Inbound domains accepted on port 25 |
|
| `postfix/relay_domains` | Inbound domains accepted on port 25 |
|
||||||
@@ -1019,8 +1158,9 @@ single-connection trade-off that follows from it.
|
|||||||
| `postfix/dmarc_recipients` | Allow-listed DMARC aggregate report addresses |
|
| `postfix/dmarc_recipients` | Allow-listed DMARC aggregate report addresses |
|
||||||
| `postfix/dmarc_transport` | Pipe transport for report ingest |
|
| `postfix/dmarc_transport` | Pipe transport for report ingest |
|
||||||
| `postfix/dmarc_relay_domains` | Domains accepted for DMARC report delivery |
|
| `postfix/dmarc_relay_domains` | Domains accepted for DMARC report delivery |
|
||||||
| `postfix/queue/` | Postfix transit mail (deferred/active); survives container recreate |
|
| `postfix/queue/` | Postfix transit mail (deferred/active); survives container recreate; excluded from backups. Handed to `postfix` / `postdrop` by name at every start, before `postfix check` (`own_queue` in `build/entrypoint.sh`): the package accounts' numbers moved in `2.8.0` (postfix 101 → 100, postdrop 103 → 102) |
|
||||||
| `log/mail.log` | Postfix delivery log + rotated copies (excluded from backups) |
|
| `log/mail.log` | Postfix delivery log + rotated copies (excluded from backups) |
|
||||||
|
| `antispam/` | The filter lists (six `.map` files) and the filter's key `filter.key`, which the spam filter's container mounts (`ANTISPAM_DIR`) |
|
||||||
| `manifest.json` | Backup version stamp; removed by the first start after a restore once its Resync is done |
|
| `manifest.json` | Backup version stamp; removed by the first start after a restore once its Resync is done |
|
||||||
| `contents.json` | A backup's last entry: the size and SHA-256 of each file under `data/`, compared by the first start after a restore before it opens the database (renamed `contents.checked.json` once compared, removed with the manifest) |
|
| `contents.json` | A backup's last entry: the size and SHA-256 of each file under `data/`, compared by the first start after a restore before it opens the database (renamed `contents.checked.json` once compared, removed with the manifest) |
|
||||||
|
|
||||||
@@ -1030,7 +1170,7 @@ full backups also archive the operator's `./certs` PEM files when present.
|
|||||||
### Database
|
### Database
|
||||||
|
|
||||||
The schema is an embedded baseline, `internal/store/migrations/0001_init.sql`,
|
The schema is an embedded baseline, `internal/store/migrations/0001_init.sql`,
|
||||||
and the migrations after it (`0002` to `0011` today; the next is `0012`),
|
and the migrations after it (`0002` to `0012` today; the next is `0013`),
|
||||||
applied by `store.Open` in file order; `PRAGMA user_version` counts the files
|
applied by `store.Open` in file order; `PRAGMA user_version` counts the files
|
||||||
applied and
|
applied and
|
||||||
`PRAGMA application_id` is `0x53503230` ("SP20"). There is no 1.x migration
|
`PRAGMA application_id` is `0x53503230` ("SP20"). There is no 1.x migration
|
||||||
@@ -1071,7 +1211,7 @@ The tables that carry the panel's model:
|
|||||||
`inbound_spam_list` (the filter lists), `dmarc_reports`,
|
`inbound_spam_list` (the filter lists), `dmarc_reports`,
|
||||||
`dmarc_report_records`, `notify_state` and `notify_addressees` (what the
|
`dmarc_report_records`, `notify_state` and `notify_addressees` (what the
|
||||||
notifier has said and to whom, § Notifications), `sender_check_days` (the
|
notifier has said and to whom, § Notifications), `sender_check_days` (the
|
||||||
sender check's downtime and the mail it deferred, per UTC day) and
|
sender check's downtime and the mail it deferred, per day of the container's zone) and
|
||||||
`delivery_log_days` (the queue sweep's measurements, § Log tailer).
|
`delivery_log_days` (the queue sweep's measurements, § Log tailer).
|
||||||
|
|
||||||
Two processes write: the panel, on one connection, and `dmarc-ingest`, which
|
Two processes write: the panel, on one connection, and `dmarc-ingest`, which
|
||||||
@@ -1089,15 +1229,19 @@ the stored log lines follow;
|
|||||||
`postfix reload` in `postrotate` — see § Log tailer above).
|
`postfix reload` in `postrotate` — see § Log tailer above).
|
||||||
|
|
||||||
**Restore:** panel button or `selfpost-backup` CLI — self-contained archive:
|
**Restore:** panel button or `selfpost-backup` CLI — self-contained archive:
|
||||||
`data/` (SQLite snapshot + tree minus `log/`, the setup token and any `tls/`
|
`data/` (SQLite snapshot + tree minus `log/`, the Postfix queue
|
||||||
under `/data`), `docker-compose.yml`, `.env`, and `certs/` when present;
|
`postfix/queue`, the copy `sasl/sasldb2.new` a password change is made on, the
|
||||||
|
setup token and any `tls/` under `/data`), `docker-compose.yml`, `.env`,
|
||||||
|
and `certs/` when present;
|
||||||
version check on restore. Requires the project directory mounted read-only at
|
version check on restore. Requires the project directory mounted read-only at
|
||||||
`SELFPOST_DEPLOY_ROOT` (`/selfpost-deploy` in the default compose file). On the
|
`SELFPOST_DEPLOY_ROOT` (`/selfpost-deploy` in the default compose file). On the
|
||||||
first successful boot after restore, the panel runs one **Resync** — OpenDKIM's
|
first successful boot after restore, the panel runs one **Resync**
|
||||||
tables, Postfix's sender map, inbound relay maps and the filter lists (when
|
(`mailStack.Resync`, `cmd/panel/httpserver.go`) — the DKIM selector table
|
||||||
`INBOUND_RELAY_ENABLE=true`), and DMARC maps (when `DMARC_REPORTS_ENABLE=true`)
|
(and the keys of deleted domains removed), Postfix's sender map, inbound relay
|
||||||
are re-derived from SQLite and both daemons are reloaded, so drift between the
|
maps and the filter lists (when `INBOUND_RELAY_ENABLE=true`), and DMARC maps
|
||||||
extracted archive and the database is healed before mail flows. Manual
|
(when `DMARC_REPORTS_ENABLE=true`) are re-derived from SQLite and Postfix is
|
||||||
|
reloaded, so drift between the extracted archive and the database is healed
|
||||||
|
before mail flows; the signer reads the new table without a reload. Manual
|
||||||
`POST /server/health/reload` (the *Reload configuration* button on Server ›
|
`POST /server/health/reload` (the *Reload configuration* button on Server ›
|
||||||
Health) writes the same files in the same order (`HandleReload`; before
|
Health) writes the same files in the same order (`HandleReload`; before
|
||||||
`2.5.4` it left out the filter lists and the DMARC maps), and on a failure
|
`2.5.4` it left out the filter lists and the DMARC maps), and on a failure
|
||||||
@@ -1164,11 +1308,15 @@ unsupported rather than as a missing doc:
|
|||||||
`POSTFIX_DMARC_RECIPIENTS` (`/data/postfix/dmarc_recipients`),
|
`POSTFIX_DMARC_RECIPIENTS` (`/data/postfix/dmarc_recipients`),
|
||||||
`POSTFIX_DMARC_TRANSPORT` (`/data/postfix/dmarc_transport`),
|
`POSTFIX_DMARC_TRANSPORT` (`/data/postfix/dmarc_transport`),
|
||||||
`POSTFIX_DMARC_RELAY_DOMAINS` (`/data/postfix/dmarc_relay_domains`),
|
`POSTFIX_DMARC_RELAY_DOMAINS` (`/data/postfix/dmarc_relay_domains`),
|
||||||
`POSTFIX_QUEUE_DIR` (`/data/postfix/queue` — set in `build/postfix-config.sh`),
|
`POSTFIX_QUEUE_DIR` (`/data/postfix/queue` — read in `build/postfix-config.sh`
|
||||||
|
for `queue_directory`; `build/entrypoint.sh` hands `/data/postfix/queue` to
|
||||||
|
Postfix by that path, so overriding it desyncs the queue's ownership),
|
||||||
`SELFPOST_DEPLOY_ROOT` (`/selfpost-deploy` — operator project directory for
|
`SELFPOST_DEPLOY_ROOT` (`/selfpost-deploy` — operator project directory for
|
||||||
full backups; mount `.:/selfpost-deploy:ro` in compose), `ANTISPAM_DIR`
|
full backups; mount `.:/selfpost-deploy:ro` in compose), `ANTISPAM_DIR`
|
||||||
(`/data/antispam` — the filter lists and the filter's key, which the spam
|
(`/data/antispam` — the filter lists and the filter's key, which the spam
|
||||||
filter's container mounts).
|
filter's container mounts). `SELFPOST_BACKUP_PASSWORD` is read by the
|
||||||
|
`selfpost-backup` CLI, not the panel: the encryption password, the other
|
||||||
|
way being `-password-file` (guide § Encrypting a backup).
|
||||||
- **Milter and Postfix startup:** `MILTER_CONNECT_TIMEOUT` (`15s`),
|
- **Milter and Postfix startup:** `MILTER_CONNECT_TIMEOUT` (`15s`),
|
||||||
`MILTER_COMMAND_TIMEOUT` (`15s`), `MILTER_CONTENT_TIMEOUT` (`30s`),
|
`MILTER_COMMAND_TIMEOUT` (`15s`), `MILTER_CONTENT_TIMEOUT` (`30s`),
|
||||||
`MILTER_WAIT_TIMEOUT` (`30` seconds).
|
`MILTER_WAIT_TIMEOUT` (`30` seconds).
|
||||||
|
|||||||
@@ -12,8 +12,8 @@
|
|||||||
<body>
|
<body>
|
||||||
<nav class="navbar is-primary" aria-label="main navigation">
|
<nav class="navbar is-primary" aria-label="main navigation">
|
||||||
<div class="container">
|
<div class="container">
|
||||||
<div class="navbar-brand"><a class="navbar-item" href="overview.html"><img src="../../selfpost-wordmark.svg" alt="SelfPost"></a></div>
|
<div class="navbar-brand"><a class="navbar-item" href="overview.html"><img src="../../selfpost-wordmark.svg" alt="SelfPost"></a><details class="sp-touchnav"><summary>Menu</summary><div class="sp-touchnav-panel"><a href="overview.html">Overview</a><details class="sp-touchnav-group"><summary>Outbound</summary><a href="out-domains.html"><i class="ti ti-world-upload"></i>Domains</a><a href="out-log.html"><i class="ti ti-list-details"></i>Log</a><a href="out-queue.html"><i class="ti ti-stack-2"></i>Queue</a><a href="dmarc.html"><i class="ti ti-report-analytics"></i>DMARC reports</a></details><details class="sp-touchnav-group"><summary>Inbound</summary><a href="in-domains.html"><i class="ti ti-world-download"></i>Domains</a><a href="in-log.html"><i class="ti ti-list-details"></i>Log</a><a href="in-queue.html"><i class="ti ti-stack-2"></i>Queue</a><a href="in-quarantine.html"><i class="ti ti-mail-pause"></i>Quarantine</a><a href="in-filter-lists.html"><i class="ti ti-filter"></i>Filter lists</a></details><details class="sp-touchnav-group"><summary>Server</summary><a href="health.html"><i class="ti ti-heartbeat"></i>Health</a><a href="preflight.html"><i class="ti ti-checklist"></i>Preflight</a><a href="system-log.html"><i class="ti ti-file-text"></i>System log</a><a href="backup.html"><i class="ti ti-archive"></i>Backup</a><a href="users.html"><i class="ti ti-users"></i>Users</a><a href="settings.html"><i class="ti ti-settings"></i>Settings</a></details><details class="sp-touchnav-group" open><summary><i class="ti ti-user-circle"></i>admin</summary><a href="account.html"><i class="ti ti-key"></i>Account</a><a href="help.html"><i class="ti ti-help"></i>Help</a><form method="post" action="login.html"><button type="submit"><i class="ti ti-logout"></i>Sign out</button></form></details></div></details></div>
|
||||||
<div class="navbar-menu is-active">
|
<div class="navbar-menu">
|
||||||
<div class="navbar-start">
|
<div class="navbar-start">
|
||||||
<a class="navbar-item" href="overview.html">Overview</a>
|
<a class="navbar-item" href="overview.html">Overview</a>
|
||||||
<div class="navbar-item has-dropdown is-hoverable">
|
<div class="navbar-item has-dropdown is-hoverable">
|
||||||
@@ -80,12 +80,18 @@
|
|||||||
<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 <span class="tag is-warning is-light">not confirmed</span></label><div class="control has-icons-left"><input class="input" id="email" name="email" type="email" value="mix@example.org" autocomplete="email"><span class="icon is-left"><i class="ti ti-mail"></i></span></div>
|
||||||
<p class="help">Your own address. A domain's report address can be filled with it from Domain settings.</p></div>
|
<p class="help">Your own address. Saving a new one sends it a link to confirm it; once confirmed it is where a link to reset your password can go. A domain's report address can be filled with it from Domain settings.</p></div>
|
||||||
|
<div class="field"><label class="label" for="pcur">Current password</label><div class="control"><input class="input" id="pcur" name="current_password" type="password" autocomplete="current-password"></div>
|
||||||
|
<p class="help">Needed only to change the e-mail.</p></div>
|
||||||
<div class="field"><label class="checkbox"><input type="checkbox" name="notify" checked> Send me notifications</label>
|
<div class="field"><label class="checkbox"><input type="checkbox" name="notify" checked> Send me notifications</label>
|
||||||
<p class="help">To this address, when something needs you: a DNS record of a domain stops matching, a rate limit refuses mail, the TLS certificate is about to expire, a message has waited in the queue for three days. <a href="help.html#notifications">What is sent and when</a></p></div>
|
<p class="help">To this address, when something needs you: a DNS record of a domain stops matching, a rate limit refuses mail, the TLS certificate is about to expire, a message has waited in the queue for three days. <a href="help.html#notifications">What is sent and when</a></p></div>
|
||||||
<button type="submit" class="button is-primary">Save profile</button>
|
<button type="submit" class="button is-primary">Save profile</button>
|
||||||
</form>
|
</form>
|
||||||
|
<form class="sp-form mt-4" method="post" action="#">
|
||||||
|
<p class="help mb-3">A link was sent to <span class="sp-mono">mix@example.org</span> at 14:02; it works once, for 24 hours.</p>
|
||||||
|
<button type="submit" class="button">Send the link again</button>
|
||||||
|
</form>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -108,7 +114,7 @@
|
|||||||
</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 · Copyright © 2026 Mikhail Yenuchenko · <a href="#">License (AGPL-3.0)</a> · <a href="#">Source</a> · No warranty</footer>
|
||||||
</div>
|
</div>
|
||||||
</main>
|
</main>
|
||||||
</body>
|
</body>
|
||||||
|
|||||||
@@ -12,8 +12,8 @@
|
|||||||
<body>
|
<body>
|
||||||
<nav class="navbar is-primary" aria-label="main navigation">
|
<nav class="navbar is-primary" aria-label="main navigation">
|
||||||
<div class="container">
|
<div class="container">
|
||||||
<div class="navbar-brand"><a class="navbar-item" href="overview.html"><img src="../../selfpost-wordmark.svg" alt="SelfPost"></a></div>
|
<div class="navbar-brand"><a class="navbar-item" href="overview.html"><img src="../../selfpost-wordmark.svg" alt="SelfPost"></a><details class="sp-touchnav"><summary>Menu</summary><div class="sp-touchnav-panel"><a href="overview.html">Overview</a><details class="sp-touchnav-group"><summary>Outbound</summary><a href="out-domains.html"><i class="ti ti-world-upload"></i>Domains</a><a href="out-log.html"><i class="ti ti-list-details"></i>Log</a><a href="out-queue.html"><i class="ti ti-stack-2"></i>Queue</a><a href="dmarc.html"><i class="ti ti-report-analytics"></i>DMARC reports</a></details><details class="sp-touchnav-group"><summary>Inbound</summary><a href="in-domains.html"><i class="ti ti-world-download"></i>Domains</a><a href="in-log.html"><i class="ti ti-list-details"></i>Log</a><a href="in-queue.html"><i class="ti ti-stack-2"></i>Queue</a><a href="in-quarantine.html"><i class="ti ti-mail-pause"></i>Quarantine</a><a href="in-filter-lists.html"><i class="ti ti-filter"></i>Filter lists</a></details><details class="sp-touchnav-group" open><summary>Server</summary><a href="health.html"><i class="ti ti-heartbeat"></i>Health</a><a href="preflight.html"><i class="ti ti-checklist"></i>Preflight</a><a href="system-log.html"><i class="ti ti-file-text"></i>System log</a><a href="backup.html" aria-current="page"><i class="ti ti-archive"></i>Backup</a><a href="users.html"><i class="ti ti-users"></i>Users</a><a href="settings.html"><i class="ti ti-settings"></i>Settings</a></details><details class="sp-touchnav-group"><summary><i class="ti ti-user-circle"></i>admin</summary><a href="account.html"><i class="ti ti-key"></i>Account</a><a href="help.html"><i class="ti ti-help"></i>Help</a><form method="post" action="login.html"><button type="submit"><i class="ti ti-logout"></i>Sign out</button></form></details></div></details></div>
|
||||||
<div class="navbar-menu is-active">
|
<div class="navbar-menu">
|
||||||
<div class="navbar-start">
|
<div class="navbar-start">
|
||||||
<a class="navbar-item" href="overview.html">Overview</a>
|
<a class="navbar-item" href="overview.html">Overview</a>
|
||||||
<div class="navbar-item has-dropdown is-hoverable">
|
<div class="navbar-item has-dropdown is-hoverable">
|
||||||
@@ -86,6 +86,8 @@
|
|||||||
<div class="field"><label class="label" for="pw2">Repeat password</label><div class="control"><input class="input" id="pw2" type="password"></div></div>
|
<div class="field"><label class="label" for="pw2">Repeat password</label><div class="control"><input class="input" id="pw2" type="password"></div></div>
|
||||||
</div>
|
</div>
|
||||||
<p class="help mb-3">Keep this password: SelfPost does not store it, and the file cannot be opened without it.</p>
|
<p class="help mb-3">Keep this password: SelfPost does not store it, and the file cannot be opened without it.</p>
|
||||||
|
<div class="field"><label class="label" for="acpw">Your account password</label><div class="control"><input class="input" id="acpw" name="account_password" type="password" autocomplete="current-password" required></div>
|
||||||
|
<p class="help">The one you sign in with, asked at every download: the archive holds every secret of the server, and a session left open must not be enough to take it.</p></div>
|
||||||
<button type="submit" class="button is-primary"><i class="ti ti-download"></i>Download full backup</button>
|
<button type="submit" class="button is-primary"><i class="ti ti-download"></i>Download full backup</button>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
@@ -109,7 +111,7 @@
|
|||||||
</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 · Copyright © 2026 Mikhail Yenuchenko · <a href="#">License (AGPL-3.0)</a> · <a href="#">Source</a> · No warranty</footer>
|
||||||
</div>
|
</div>
|
||||||
</main>
|
</main>
|
||||||
</body>
|
</body>
|
||||||
|
|||||||
@@ -52,15 +52,39 @@ HEAD = """<!DOCTYPE html>
|
|||||||
</head>
|
</head>
|
||||||
"""
|
"""
|
||||||
|
|
||||||
FOOT = ('<footer class="sp-foot">SelfPost 1.8.0 · © 2026 Mix · <a href="#">License (AGPL-3.0)</a>'
|
FOOT = ('<footer class="sp-foot">SelfPost 1.8.0 · Copyright © 2026 Mikhail Yenuchenko · <a href="#">License (AGPL-3.0)</a>'
|
||||||
' · <a href="#">Source</a> · No warranty</footer>')
|
' · <a href="#">Source</a> · No warranty</footer>')
|
||||||
|
|
||||||
|
|
||||||
|
def touchnav(section, page):
|
||||||
|
"""The navigation below 1024 px: a Menu that opens over the page, its
|
||||||
|
sections folded but the current one - details elements, no script."""
|
||||||
|
out = ['<details class="sp-touchnav"><summary>Menu</summary><div class="sp-touchnav-panel">']
|
||||||
|
for key, label, href, _icon, pages in NAV:
|
||||||
|
if not pages:
|
||||||
|
cur = ' aria-current="page"' if key == section else ""
|
||||||
|
out.append(f'<a href="{href}"{cur}>{label}</a>')
|
||||||
|
continue
|
||||||
|
opened = " open" if key == section else ""
|
||||||
|
out.append(f'<details class="sp-touchnav-group"{opened}><summary>{label}</summary>')
|
||||||
|
for pkey, plabel, phref, picon in pages:
|
||||||
|
cur = ' aria-current="page"' if key == section and pkey == page else ""
|
||||||
|
out.append(f'<a href="{phref}"{cur}><i class="ti {picon}"></i>{plabel}</a>')
|
||||||
|
out.append('</details>')
|
||||||
|
opened = " open" if section == "user" else ""
|
||||||
|
out += [f'<details class="sp-touchnav-group"{opened}><summary><i class="ti ti-user-circle"></i>admin</summary>',
|
||||||
|
'<a href="account.html"><i class="ti ti-key"></i>Account</a>',
|
||||||
|
'<a href="help.html"><i class="ti ti-help"></i>Help</a>',
|
||||||
|
'<form method="post" action="login.html"><button type="submit"><i class="ti ti-logout"></i>Sign out</button></form>',
|
||||||
|
'</details>', '</div></details>']
|
||||||
|
return "".join(out)
|
||||||
|
|
||||||
|
|
||||||
def navbar(section, page):
|
def navbar(section, page):
|
||||||
out = ['<nav class="navbar is-primary" aria-label="main navigation">', ' <div class="container">',
|
out = ['<nav class="navbar is-primary" aria-label="main navigation">', ' <div class="container">',
|
||||||
' <div class="navbar-brand"><a class="navbar-item" href="overview.html">'
|
' <div class="navbar-brand"><a class="navbar-item" href="overview.html">'
|
||||||
'<img src="../../selfpost-wordmark.svg" alt="SelfPost"></a></div>',
|
'<img src="../../selfpost-wordmark.svg" alt="SelfPost"></a>' + touchnav(section, page) + '</div>',
|
||||||
' <div class="navbar-menu is-active">', ' <div class="navbar-start">']
|
' <div class="navbar-menu">', ' <div class="navbar-start">']
|
||||||
for key, label, href, _icon, pages in NAV:
|
for key, label, href, _icon, pages in NAV:
|
||||||
cur = " sp-current" if key == section else ""
|
cur = " sp-current" if key == section else ""
|
||||||
if not pages:
|
if not pages:
|
||||||
|
|||||||
@@ -12,8 +12,8 @@
|
|||||||
<body>
|
<body>
|
||||||
<nav class="navbar is-primary" aria-label="main navigation">
|
<nav class="navbar is-primary" aria-label="main navigation">
|
||||||
<div class="container">
|
<div class="container">
|
||||||
<div class="navbar-brand"><a class="navbar-item" href="overview.html"><img src="../../selfpost-wordmark.svg" alt="SelfPost"></a></div>
|
<div class="navbar-brand"><a class="navbar-item" href="overview.html"><img src="../../selfpost-wordmark.svg" alt="SelfPost"></a><details class="sp-touchnav"><summary>Menu</summary><div class="sp-touchnav-panel"><a href="overview.html">Overview</a><details class="sp-touchnav-group"><summary>Outbound</summary><a href="out-domains.html"><i class="ti ti-world-upload"></i>Domains</a><a href="out-log.html"><i class="ti ti-list-details"></i>Log</a><a href="out-queue.html"><i class="ti ti-stack-2"></i>Queue</a><a href="dmarc.html"><i class="ti ti-report-analytics"></i>DMARC reports</a></details><details class="sp-touchnav-group"><summary>Inbound</summary><a href="in-domains.html"><i class="ti ti-world-download"></i>Domains</a><a href="in-log.html"><i class="ti ti-list-details"></i>Log</a><a href="in-queue.html"><i class="ti ti-stack-2"></i>Queue</a><a href="in-quarantine.html"><i class="ti ti-mail-pause"></i>Quarantine</a><a href="in-filter-lists.html"><i class="ti ti-filter"></i>Filter lists</a></details><details class="sp-touchnav-group"><summary>Server</summary><a href="health.html"><i class="ti ti-heartbeat"></i>Health</a><a href="preflight.html"><i class="ti ti-checklist"></i>Preflight</a><a href="system-log.html"><i class="ti ti-file-text"></i>System log</a><a href="backup.html"><i class="ti ti-archive"></i>Backup</a><a href="users.html"><i class="ti ti-users"></i>Users</a><a href="settings.html"><i class="ti ti-settings"></i>Settings</a></details><details class="sp-touchnav-group"><summary><i class="ti ti-user-circle"></i>admin</summary><a href="account.html"><i class="ti ti-key"></i>Account</a><a href="help.html"><i class="ti ti-help"></i>Help</a><form method="post" action="login.html"><button type="submit"><i class="ti ti-logout"></i>Sign out</button></form></details></div></details></div>
|
||||||
<div class="navbar-menu is-active">
|
<div class="navbar-menu">
|
||||||
<div class="navbar-start">
|
<div class="navbar-start">
|
||||||
<a class="navbar-item" href="overview.html">Overview</a>
|
<a class="navbar-item" href="overview.html">Overview</a>
|
||||||
<div class="navbar-item has-dropdown is-hoverable">
|
<div class="navbar-item has-dropdown is-hoverable">
|
||||||
@@ -150,9 +150,9 @@
|
|||||||
<div class="columns">
|
<div class="columns">
|
||||||
<div class="column is-7">
|
<div class="column is-7">
|
||||||
<div class="box">
|
<div class="box">
|
||||||
<div class="sp-box-head"><span class="sp-no">05</span><h2>dns_record · copy_field</h2><div class="sp-end">One block per record</div></div>
|
<div class="sp-box-head"><span class="sp-no">05</span><h2>dns_record · copy_field</h2><div class="sp-end">One block per record · folded when DNS holds it as shown</div></div>
|
||||||
<div class="sp-record">
|
<details class="sp-record">
|
||||||
<div class="sp-record-head"><h3>Record</h3><span class="tag is-success is-light">published</span><small>A one-line note</small></div>
|
<summary class="sp-record-head"><h3>Record</h3><span class="tag is-success is-light">published</span><small>A one-line note</small><span class="sp-record-toggle"><i class="ti ti-eye"></i><span>Show values</span><span>Hide values</span></span></summary>
|
||||||
<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="copy_field — single line"></div><div class="control"><button type="button" class="button"><i class="ti ti-copy"></i>Copy</button></div></div></div>
|
<div class="field has-addons"><div class="control is-expanded"><input class="input" readonly value="copy_field — single line"></div><div class="control"><button type="button" class="button"><i class="ti ti-copy"></i>Copy</button></div></div></div>
|
||||||
@@ -160,7 +160,7 @@
|
|||||||
</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"><textarea class="textarea" rows="2" readonly>copy_field — multi-line, for a DKIM key</textarea></div><div class="control"><button type="button" class="button"><i class="ti ti-copy"></i>Copy</button></div></div></div>
|
<div class="field has-addons"><div class="control is-expanded"><textarea class="textarea" rows="2" readonly>copy_field — multi-line, for a DKIM key</textarea></div><div class="control"><button type="button" class="button"><i class="ti ti-copy"></i>Copy</button></div></div></div>
|
||||||
</div>
|
</details>
|
||||||
<div class="sp-record">
|
<div class="sp-record">
|
||||||
<div class="sp-record-head"><h3>Record</h3><span class="tag is-danger is-light">mismatch</span><small>"In DNS now" appears only when it differs</small></div>
|
<div class="sp-record-head"><h3>Record</h3><span class="tag is-danger is-light">mismatch</span><small>"In DNS now" appears only when it differs</small></div>
|
||||||
<div class="field"><label class="label">In DNS now</label>
|
<div class="field"><label class="label">In DNS now</label>
|
||||||
@@ -172,7 +172,7 @@
|
|||||||
<div class="box">
|
<div class="box">
|
||||||
<div class="sp-box-head"><span class="sp-no">07</span><h2>Table conventions</h2><div class="sp-end">Always inside <code>table-container</code> · an address breaks after its @</div></div>
|
<div class="sp-box-head"><span class="sp-no">07</span><h2>Table conventions</h2><div class="sp-end">Always inside <code>table-container</code> · an address breaks after its @</div></div>
|
||||||
<div class="table-container"><table class="table is-fullwidth is-hoverable">
|
<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</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><form method="post" action="#"><button type="submit" class="has-text-danger">Delete</button></form></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>
|
||||||
@@ -182,9 +182,10 @@
|
|||||||
|
|
||||||
<div class="box">
|
<div class="box">
|
||||||
<div class="sp-box-head"><span class="sp-no">09</span><h2>log_pane</h2><div class="sp-end">sp-d time · sp-w warning · sp-e error</div></div>
|
<div class="sp-box-head"><span class="sp-no">09</span><h2>log_pane</h2><div class="sp-end">sp-d time · sp-w warning · sp-e error</div></div>
|
||||||
<pre class="sp-log"><span class="sp-d">13:58:07</span> an ordinary line
|
<pre class="sp-log"><span class="sp-ll"><span class="sp-d">13:58:07</span> an ordinary line
|
||||||
<span class="sp-d">13:58:09</span> <span class="sp-w">a deferred delivery — sp-w</span>
|
</span><span class="sp-ll"><span class="sp-d">13:58:09</span> <span class="sp-w">a deferred delivery — sp-w</span>
|
||||||
<span class="sp-d">13:57:41</span> <span class="sp-e">a reject or a bounce — sp-e</span></pre>
|
</span><span class="sp-ll"><span class="sp-d">13:57:41</span> <span class="sp-e">a reject or a bounce — sp-e</span>
|
||||||
|
</span></pre>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -195,7 +196,9 @@
|
|||||||
<dl class="sp-facts">
|
<dl class="sp-facts">
|
||||||
<div><dt>Label</dt><dd>Value</dd></div>
|
<div><dt>Label</dt><dd>Value</dd></div>
|
||||||
<div><dt>Mono value</dt><dd class="sp-mono">4XcB7k2Jm9z1</dd></div>
|
<div><dt>Mono value</dt><dd class="sp-mono">4XcB7k2Jm9z1</dd></div>
|
||||||
|
<div><dt>Label with a tag <span class="tag is-success is-light">matches</span></dt><dd>a verdict on the value</dd></div>
|
||||||
<div><dt>sp-big</dt><dd class="sp-big">5 days</dd><dd class="sp-muted">a supporting line</dd></div>
|
<div><dt>sp-big</dt><dd class="sp-big">5 days</dd><dd class="sp-muted">a supporting line</dd></div>
|
||||||
|
<div class="sp-wide"><dt>sp-wide</dt><dd class="sp-mono">a value that takes the whole row, a line of a server's own</dd></div>
|
||||||
</dl>
|
</dl>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -204,9 +207,9 @@
|
|||||||
<div class="sp-box-head"><span class="sp-no">08</span><h2>timeline</h2></div>
|
<div class="sp-box-head"><span class="sp-no">08</span><h2>timeline</h2></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>Done</strong> — default</li>
|
<li><time>13:58:07</time><strong>Done</strong> — default</li>
|
||||||
<li class="sp-warn"><time>13:58:09 UTC</time><strong>Deferred</strong> — sp-warn</li>
|
<li class="sp-warn"><time>13:58:09</time><strong>Deferred</strong> — sp-warn</li>
|
||||||
<li class="sp-fail"><time>14:40:00 UTC</time><strong>Bounced</strong> — sp-fail</li>
|
<li class="sp-fail"><time>14:40:00</time><strong>Bounced</strong> — sp-fail</li>
|
||||||
<li class="sp-pending"><time>not yet</time>Expected next — sp-pending</li>
|
<li class="sp-pending"><time>not yet</time>Expected next — sp-pending</li>
|
||||||
</ol>
|
</ol>
|
||||||
</div>
|
</div>
|
||||||
@@ -289,7 +292,7 @@
|
|||||||
<div class="sp-box-foot">Bulma's <code>is-small</code> and <code>is-size-7</code> are not used: a control is never small, and <code>sp-small</code> replaces the helper. The postmark's fine print is an imprint, not text.</div>
|
<div class="sp-box-foot">Bulma's <code>is-small</code> and <code>is-size-7</code> are not used: a control is never small, and <code>sp-small</code> replaces the helper. The postmark's fine print is an imprint, not text.</div>
|
||||||
</div>
|
</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 · Copyright © 2026 Mikhail Yenuchenko · <a href="#">License (AGPL-3.0)</a> · <a href="#">Source</a> · No warranty</footer>
|
||||||
</div>
|
</div>
|
||||||
</main>
|
</main>
|
||||||
</body>
|
</body>
|
||||||
|
|||||||
@@ -0,0 +1,32 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en" data-theme="light">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
|
<title>Confirm e-mail · 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">Confirm e-mail</h1>
|
||||||
|
<div class="notification is-danger is-light"><i class="ti ti-alert-circle"></i>Sign in to the account in this browser, then open the link again.</div>
|
||||||
|
<p><a href="login.html">Sign in</a></p>
|
||||||
|
</div>
|
||||||
|
<p class="sp-foot">Copyright © 2026 Mikhail Yenuchenko · <a href="#">License (AGPL-3.0)</a> · <a href="#">Source</a> · No warranty</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en" data-theme="light">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
|
<title>Confirm e-mail · SelfPost</title>
|
||||||
|
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bulma@1.0.4/css/bulma.min.css">
|
||||||
|
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@tabler/icons-webfont@3/dist/tabler-icons.min.css">
|
||||||
|
<link rel="stylesheet" href="../shared/brand.css">
|
||||||
|
<link rel="stylesheet" href="theme.css">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="columns is-gapless sp-signin">
|
||||||
|
<div class="column is-half sp-signin-brand">
|
||||||
|
<div>
|
||||||
|
<img src="../../selfpost-stamp.svg" alt="SelfPost">
|
||||||
|
<p>YOUR SMTP. YOUR RULES.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="column is-half sp-signin-form">
|
||||||
|
<form class="sp-form" method="post" action="account.html">
|
||||||
|
<p class="sp-kicker">mail.example.org · admin</p>
|
||||||
|
<h1 class="title is-3">Confirm e-mail</h1>
|
||||||
|
<p class="sp-muted mb-4"><span class="sp-mono">mix@example.org</span> becomes the confirmed address of this account: where a link to reset its password can be sent.</p>
|
||||||
|
<div class="field mt-5"><button type="submit" class="button is-primary is-fullwidth">Confirm</button></div>
|
||||||
|
</form>
|
||||||
|
<p class="sp-foot">Copyright © 2026 Mikhail Yenuchenko · <a href="#">License (AGPL-3.0)</a> · <a href="#">Source</a> · No warranty</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -12,8 +12,8 @@
|
|||||||
<body>
|
<body>
|
||||||
<nav class="navbar is-primary" aria-label="main navigation">
|
<nav class="navbar is-primary" aria-label="main navigation">
|
||||||
<div class="container">
|
<div class="container">
|
||||||
<div class="navbar-brand"><a class="navbar-item" href="overview.html"><img src="../../selfpost-wordmark.svg" alt="SelfPost"></a></div>
|
<div class="navbar-brand"><a class="navbar-item" href="overview.html"><img src="../../selfpost-wordmark.svg" alt="SelfPost"></a><details class="sp-touchnav"><summary>Menu</summary><div class="sp-touchnav-panel"><a href="overview.html">Overview</a><details class="sp-touchnav-group" open><summary>Outbound</summary><a href="out-domains.html"><i class="ti ti-world-upload"></i>Domains</a><a href="out-log.html"><i class="ti ti-list-details"></i>Log</a><a href="out-queue.html"><i class="ti ti-stack-2"></i>Queue</a><a href="dmarc.html" aria-current="page"><i class="ti ti-report-analytics"></i>DMARC reports</a></details><details class="sp-touchnav-group"><summary>Inbound</summary><a href="in-domains.html"><i class="ti ti-world-download"></i>Domains</a><a href="in-log.html"><i class="ti ti-list-details"></i>Log</a><a href="in-queue.html"><i class="ti ti-stack-2"></i>Queue</a><a href="in-quarantine.html"><i class="ti ti-mail-pause"></i>Quarantine</a><a href="in-filter-lists.html"><i class="ti ti-filter"></i>Filter lists</a></details><details class="sp-touchnav-group"><summary>Server</summary><a href="health.html"><i class="ti ti-heartbeat"></i>Health</a><a href="preflight.html"><i class="ti ti-checklist"></i>Preflight</a><a href="system-log.html"><i class="ti ti-file-text"></i>System log</a><a href="backup.html"><i class="ti ti-archive"></i>Backup</a><a href="users.html"><i class="ti ti-users"></i>Users</a><a href="settings.html"><i class="ti ti-settings"></i>Settings</a></details><details class="sp-touchnav-group"><summary><i class="ti ti-user-circle"></i>admin</summary><a href="account.html"><i class="ti ti-key"></i>Account</a><a href="help.html"><i class="ti ti-help"></i>Help</a><form method="post" action="login.html"><button type="submit"><i class="ti ti-logout"></i>Sign out</button></form></details></div></details></div>
|
||||||
<div class="navbar-menu is-active">
|
<div class="navbar-menu">
|
||||||
<div class="navbar-start">
|
<div class="navbar-start">
|
||||||
<a class="navbar-item" href="overview.html">Overview</a>
|
<a class="navbar-item" href="overview.html">Overview</a>
|
||||||
<div class="navbar-item has-dropdown is-hoverable sp-current">
|
<div class="navbar-item has-dropdown is-hoverable sp-current">
|
||||||
@@ -92,7 +92,7 @@
|
|||||||
<div class="box">
|
<div class="box">
|
||||||
<div class="sp-box-head"><span class="sp-no">02</span><h2>Reports</h2></div>
|
<div class="sp-box-head"><span class="sp-no">02</span><h2>Reports</h2></div>
|
||||||
<div class="table-container"><table class="table is-fullwidth is-hoverable">
|
<div class="table-container"><table class="table is-fullwidth is-hoverable">
|
||||||
<thead><tr><th>Received, UTC</th><th>Reporter</th><th>Window</th><th>Pass</th><th>Fail</th><th></th></tr></thead>
|
<thead><tr><th>Received</th><th>Reporter</th><th>Window</th><th>Pass</th><th>Fail</th><th></th></tr></thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
<tr><td class="sp-mono sp-muted sp-nowrap sp-small">09-21 04:40</td><td class="sp-muted">Outlook.com</td><td>20 Sep</td><td>1 903</td><td><span class="tag is-warning is-light">7</span></td><td class="sp-actions"><a href="dmarc-report.html">View</a></td></tr>
|
<tr><td class="sp-mono sp-muted sp-nowrap sp-small">09-21 04:40</td><td class="sp-muted">Outlook.com</td><td>20 Sep</td><td>1 903</td><td><span class="tag is-warning is-light">7</span></td><td class="sp-actions"><a href="dmarc-report.html">View</a></td></tr>
|
||||||
<tr><td class="sp-mono sp-muted sp-nowrap sp-small">09-21 02:15</td><td class="sp-muted">google.com</td><td>20 Sep</td><td>6 410</td><td>0</td><td class="sp-actions"><a href="dmarc-report.html">View</a></td></tr>
|
<tr><td class="sp-mono sp-muted sp-nowrap sp-small">09-21 02:15</td><td class="sp-muted">google.com</td><td>20 Sep</td><td>6 410</td><td>0</td><td class="sp-actions"><a href="dmarc-report.html">View</a></td></tr>
|
||||||
@@ -103,7 +103,7 @@
|
|||||||
</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 · Copyright © 2026 Mikhail Yenuchenko · <a href="#">License (AGPL-3.0)</a> · <a href="#">Source</a> · No warranty</footer>
|
||||||
</div>
|
</div>
|
||||||
</main>
|
</main>
|
||||||
</body>
|
</body>
|
||||||
|
|||||||
@@ -12,8 +12,8 @@
|
|||||||
<body>
|
<body>
|
||||||
<nav class="navbar is-primary" aria-label="main navigation">
|
<nav class="navbar is-primary" aria-label="main navigation">
|
||||||
<div class="container">
|
<div class="container">
|
||||||
<div class="navbar-brand"><a class="navbar-item" href="overview.html"><img src="../../selfpost-wordmark.svg" alt="SelfPost"></a></div>
|
<div class="navbar-brand"><a class="navbar-item" href="overview.html"><img src="../../selfpost-wordmark.svg" alt="SelfPost"></a><details class="sp-touchnav"><summary>Menu</summary><div class="sp-touchnav-panel"><a href="overview.html">Overview</a><details class="sp-touchnav-group" open><summary>Outbound</summary><a href="out-domains.html"><i class="ti ti-world-upload"></i>Domains</a><a href="out-log.html"><i class="ti ti-list-details"></i>Log</a><a href="out-queue.html"><i class="ti ti-stack-2"></i>Queue</a><a href="dmarc.html" aria-current="page"><i class="ti ti-report-analytics"></i>DMARC reports</a></details><details class="sp-touchnav-group"><summary>Inbound</summary><a href="in-domains.html"><i class="ti ti-world-download"></i>Domains</a><a href="in-log.html"><i class="ti ti-list-details"></i>Log</a><a href="in-queue.html"><i class="ti ti-stack-2"></i>Queue</a><a href="in-quarantine.html"><i class="ti ti-mail-pause"></i>Quarantine</a><a href="in-filter-lists.html"><i class="ti ti-filter"></i>Filter lists</a></details><details class="sp-touchnav-group"><summary>Server</summary><a href="health.html"><i class="ti ti-heartbeat"></i>Health</a><a href="preflight.html"><i class="ti ti-checklist"></i>Preflight</a><a href="system-log.html"><i class="ti ti-file-text"></i>System log</a><a href="backup.html"><i class="ti ti-archive"></i>Backup</a><a href="users.html"><i class="ti ti-users"></i>Users</a><a href="settings.html"><i class="ti ti-settings"></i>Settings</a></details><details class="sp-touchnav-group"><summary><i class="ti ti-user-circle"></i>admin</summary><a href="account.html"><i class="ti ti-key"></i>Account</a><a href="help.html"><i class="ti ti-help"></i>Help</a><form method="post" action="login.html"><button type="submit"><i class="ti ti-logout"></i>Sign out</button></form></details></div></details></div>
|
||||||
<div class="navbar-menu is-active">
|
<div class="navbar-menu">
|
||||||
<div class="navbar-start">
|
<div class="navbar-start">
|
||||||
<a class="navbar-item" href="overview.html">Overview</a>
|
<a class="navbar-item" href="overview.html">Overview</a>
|
||||||
<div class="navbar-item has-dropdown is-hoverable sp-current">
|
<div class="navbar-item has-dropdown is-hoverable sp-current">
|
||||||
@@ -68,7 +68,7 @@
|
|||||||
<div class="sp-head">
|
<div class="sp-head">
|
||||||
<div class="sp-grow">
|
<div class="sp-grow">
|
||||||
<p class="sp-kicker"><a href="dmarc.html">DMARC reports</a> / <a href="dmarc-domain.html">shop.example.org</a> /</p>
|
<p class="sp-kicker"><a href="dmarc.html">DMARC reports</a> / <a href="dmarc-domain.html">shop.example.org</a> /</p>
|
||||||
<h1 class="title is-4">Outlook.com · 20 Sep 2026</h1>
|
<h1 class="title is-4">Outlook.com · 20 Sep</h1>
|
||||||
<p class="sp-route"><span>shop.example.org</span><span class="tag is-success is-light">1 903 pass</span><span class="tag is-warning is-light">7 fail</span></p>
|
<p class="sp-route"><span>shop.example.org</span><span class="tag is-success is-light">1 903 pass</span><span class="tag is-warning is-light">7 fail</span></p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -81,8 +81,8 @@
|
|||||||
<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>
|
||||||
<div><dt>Report id</dt><dd class="sp-mono sp-small">5f1c9a0e7b2d4e61a3</dd></div>
|
<div><dt>Report id</dt><dd class="sp-mono sp-small">5f1c9a0e7b2d4e61a3</dd></div>
|
||||||
<div><dt>Window</dt><dd>20 Sep 00:00 – 21 Sep 00:00 UTC</dd></div>
|
<div><dt>Window</dt><dd>20 Sep 03:00 – 21 Sep 03:00</dd><dd class="sp-muted">The reporter's day, 20 Sep: reports count their days in UTC, and the title and the lists name that day. The times are this panel's zone.</dd></div>
|
||||||
<div><dt>Received</dt><dd>2026-09-21 04:40 UTC</dd></div>
|
<div><dt>Received</dt><dd>2026-09-21 04:40</dd></div>
|
||||||
</dl>
|
</dl>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -112,7 +112,7 @@
|
|||||||
</table></div>
|
</table></div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<footer class="sp-foot">SelfPost 1.8.0 · © 2026 Mix · <a href="#">License (AGPL-3.0)</a> · <a href="#">Source</a> · No warranty</footer>
|
<footer class="sp-foot">SelfPost 1.8.0 · Copyright © 2026 Mikhail Yenuchenko · <a href="#">License (AGPL-3.0)</a> · <a href="#">Source</a> · No warranty</footer>
|
||||||
</div>
|
</div>
|
||||||
</main>
|
</main>
|
||||||
</body>
|
</body>
|
||||||
|
|||||||
@@ -12,8 +12,8 @@
|
|||||||
<body>
|
<body>
|
||||||
<nav class="navbar is-primary" aria-label="main navigation">
|
<nav class="navbar is-primary" aria-label="main navigation">
|
||||||
<div class="container">
|
<div class="container">
|
||||||
<div class="navbar-brand"><a class="navbar-item" href="overview.html"><img src="../../selfpost-wordmark.svg" alt="SelfPost"></a></div>
|
<div class="navbar-brand"><a class="navbar-item" href="overview.html"><img src="../../selfpost-wordmark.svg" alt="SelfPost"></a><details class="sp-touchnav"><summary>Menu</summary><div class="sp-touchnav-panel"><a href="overview.html">Overview</a><details class="sp-touchnav-group" open><summary>Outbound</summary><a href="out-domains.html"><i class="ti ti-world-upload"></i>Domains</a><a href="out-log.html"><i class="ti ti-list-details"></i>Log</a><a href="out-queue.html"><i class="ti ti-stack-2"></i>Queue</a><a href="dmarc.html" aria-current="page"><i class="ti ti-report-analytics"></i>DMARC reports</a></details><details class="sp-touchnav-group"><summary>Inbound</summary><a href="in-domains.html"><i class="ti ti-world-download"></i>Domains</a><a href="in-log.html"><i class="ti ti-list-details"></i>Log</a><a href="in-queue.html"><i class="ti ti-stack-2"></i>Queue</a><a href="in-quarantine.html"><i class="ti ti-mail-pause"></i>Quarantine</a><a href="in-filter-lists.html"><i class="ti ti-filter"></i>Filter lists</a></details><details class="sp-touchnav-group"><summary>Server</summary><a href="health.html"><i class="ti ti-heartbeat"></i>Health</a><a href="preflight.html"><i class="ti ti-checklist"></i>Preflight</a><a href="system-log.html"><i class="ti ti-file-text"></i>System log</a><a href="backup.html"><i class="ti ti-archive"></i>Backup</a><a href="users.html"><i class="ti ti-users"></i>Users</a><a href="settings.html"><i class="ti ti-settings"></i>Settings</a></details><details class="sp-touchnav-group"><summary><i class="ti ti-user-circle"></i>admin</summary><a href="account.html"><i class="ti ti-key"></i>Account</a><a href="help.html"><i class="ti ti-help"></i>Help</a><form method="post" action="login.html"><button type="submit"><i class="ti ti-logout"></i>Sign out</button></form></details></div></details></div>
|
||||||
<div class="navbar-menu is-active">
|
<div class="navbar-menu">
|
||||||
<div class="navbar-start">
|
<div class="navbar-start">
|
||||||
<a class="navbar-item" href="overview.html">Overview</a>
|
<a class="navbar-item" href="overview.html">Overview</a>
|
||||||
<div class="navbar-item has-dropdown is-hoverable sp-current">
|
<div class="navbar-item has-dropdown is-hoverable sp-current">
|
||||||
@@ -77,7 +77,7 @@
|
|||||||
<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-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</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 addresses</dt><dd class="sp-mono sp-small">dmarc-reports+<domain>@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 a domain or 90 days</dd></div>
|
<div><dt>Retention</dt><dd>500 reports a domain or 90 days</dd></div>
|
||||||
@@ -88,7 +88,7 @@
|
|||||||
<div class="box">
|
<div class="box">
|
||||||
<div class="sp-box-head"><span class="sp-no">02</span><h2>Recent reports</h2></div>
|
<div class="sp-box-head"><span class="sp-no">02</span><h2>Recent reports</h2></div>
|
||||||
<div class="table-container"><table class="table is-fullwidth is-hoverable">
|
<div class="table-container"><table class="table is-fullwidth is-hoverable">
|
||||||
<thead><tr><th>Received, UTC</th><th>Domain</th><th>Reporter</th><th>Window</th><th>Pass</th><th>Fail</th><th></th></tr></thead>
|
<thead><tr><th>Received</th><th>Domain</th><th>Reporter</th><th>Window</th><th>Pass</th><th>Fail</th><th></th></tr></thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
<tr><td class="sp-mono sp-muted sp-nowrap sp-small">09-21 06:12</td><td><a href="dmarc-domain.html">example.org</a></td><td class="sp-muted">google.com</td><td>20 Sep</td><td>412</td><td>0</td><td class="sp-actions"><a href="dmarc-report.html">View</a></td></tr>
|
<tr><td class="sp-mono sp-muted sp-nowrap sp-small">09-21 06:12</td><td><a href="dmarc-domain.html">example.org</a></td><td class="sp-muted">google.com</td><td>20 Sep</td><td>412</td><td>0</td><td class="sp-actions"><a href="dmarc-report.html">View</a></td></tr>
|
||||||
<tr><td class="sp-mono sp-muted sp-nowrap sp-small">09-21 04:40</td><td><a href="dmarc-domain.html">shop.example.org</a></td><td class="sp-muted">Outlook.com</td><td>20 Sep</td><td>1 903</td><td><span class="tag is-warning is-light">7</span></td><td class="sp-actions"><a href="dmarc-report.html">View</a></td></tr>
|
<tr><td class="sp-mono sp-muted sp-nowrap sp-small">09-21 04:40</td><td><a href="dmarc-domain.html">shop.example.org</a></td><td class="sp-muted">Outlook.com</td><td>20 Sep</td><td>1 903</td><td><span class="tag is-warning is-light">7</span></td><td class="sp-actions"><a href="dmarc-report.html">View</a></td></tr>
|
||||||
@@ -98,7 +98,7 @@
|
|||||||
</table></div>
|
</table></div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<footer class="sp-foot">SelfPost 1.8.0 · © 2026 Mix · <a href="#">License (AGPL-3.0)</a> · <a href="#">Source</a> · No warranty</footer>
|
<footer class="sp-foot">SelfPost 1.8.0 · Copyright © 2026 Mikhail Yenuchenko · <a href="#">License (AGPL-3.0)</a> · <a href="#">Source</a> · No warranty</footer>
|
||||||
</div>
|
</div>
|
||||||
</main>
|
</main>
|
||||||
</body>
|
</body>
|
||||||
|
|||||||
@@ -12,8 +12,8 @@
|
|||||||
<body>
|
<body>
|
||||||
<nav class="navbar is-primary" aria-label="main navigation">
|
<nav class="navbar is-primary" aria-label="main navigation">
|
||||||
<div class="container">
|
<div class="container">
|
||||||
<div class="navbar-brand"><a class="navbar-item" href="overview.html"><img src="../../selfpost-wordmark.svg" alt="SelfPost"></a></div>
|
<div class="navbar-brand"><a class="navbar-item" href="overview.html"><img src="../../selfpost-wordmark.svg" alt="SelfPost"></a><details class="sp-touchnav"><summary>Menu</summary><div class="sp-touchnav-panel"><a href="overview.html">Overview</a><details class="sp-touchnav-group"><summary>Outbound</summary><a href="out-domains.html"><i class="ti ti-world-upload"></i>Domains</a><a href="out-log.html"><i class="ti ti-list-details"></i>Log</a><a href="out-queue.html"><i class="ti ti-stack-2"></i>Queue</a><a href="dmarc.html"><i class="ti ti-report-analytics"></i>DMARC reports</a></details><details class="sp-touchnav-group"><summary>Inbound</summary><a href="in-domains.html"><i class="ti ti-world-download"></i>Domains</a><a href="in-log.html"><i class="ti ti-list-details"></i>Log</a><a href="in-queue.html"><i class="ti ti-stack-2"></i>Queue</a><a href="in-quarantine.html"><i class="ti ti-mail-pause"></i>Quarantine</a><a href="in-filter-lists.html"><i class="ti ti-filter"></i>Filter lists</a></details><details class="sp-touchnav-group" open><summary>Server</summary><a href="health.html" aria-current="page"><i class="ti ti-heartbeat"></i>Health</a><a href="preflight.html"><i class="ti ti-checklist"></i>Preflight</a><a href="system-log.html"><i class="ti ti-file-text"></i>System log</a><a href="backup.html"><i class="ti ti-archive"></i>Backup</a><a href="users.html"><i class="ti ti-users"></i>Users</a><a href="settings.html"><i class="ti ti-settings"></i>Settings</a></details><details class="sp-touchnav-group"><summary><i class="ti ti-user-circle"></i>admin</summary><a href="account.html"><i class="ti ti-key"></i>Account</a><a href="help.html"><i class="ti ti-help"></i>Help</a><form method="post" action="login.html"><button type="submit"><i class="ti ti-logout"></i>Sign out</button></form></details></div></details></div>
|
||||||
<div class="navbar-menu is-active">
|
<div class="navbar-menu">
|
||||||
<div class="navbar-start">
|
<div class="navbar-start">
|
||||||
<a class="navbar-item" href="overview.html">Overview</a>
|
<a class="navbar-item" href="overview.html">Overview</a>
|
||||||
<div class="navbar-item has-dropdown is-hoverable">
|
<div class="navbar-item has-dropdown is-hoverable">
|
||||||
@@ -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">Health</h1>
|
<h1 class="title is-3">Health</h1>
|
||||||
<p class="sp-lead">Everything behind the six cards on Overview. Refreshes every 10 s.</p>
|
<p class="sp-lead">Everything behind the six cards on Overview. Refreshes automatically.</p>
|
||||||
</div>
|
</div>
|
||||||
<form method="post" action="#"><button type="submit" class="button"><i class="ti ti-refresh"></i>Re-check DNS</button></form>
|
<form method="post" action="#"><button type="submit" class="button"><i class="ti ti-refresh"></i>Re-check DNS</button></form>
|
||||||
</div>
|
</div>
|
||||||
@@ -108,6 +108,7 @@
|
|||||||
<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>
|
||||||
<div><dt>Lookup</dt><dd class="sp-mono">→ 203.0.113.25<br>← mail.example.org</dd></div>
|
<div><dt>Lookup</dt><dd class="sp-mono">→ 203.0.113.25<br>← mail.example.org</dd></div>
|
||||||
|
<div class="sp-wide"><dt>Greeting</dt><dd class="sp-mono">220 mail.example.org ESMTP</dd></div>
|
||||||
</dl>
|
</dl>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -133,6 +134,7 @@
|
|||||||
<tr><td>dkim-signer <span class="sp-muted">· notifications</span><br><span class="sp-muted sp-mono sp-small">unix:/run/selfpost/sign-local.sock</span></td><td><span class="tag is-success is-light">ok</span></td></tr>
|
<tr><td>dkim-signer <span class="sp-muted">· notifications</span><br><span class="sp-muted sp-mono sp-small">unix:/run/selfpost/sign-local.sock</span></td><td><span class="tag is-success is-light">ok</span></td></tr>
|
||||||
<tr><td>journal-milter<br><span class="sp-muted sp-mono sp-small">unix:/run/selfpost/journal.sock</span></td><td><span class="tag is-success is-light">ok</span></td></tr>
|
<tr><td>journal-milter<br><span class="sp-muted sp-mono sp-small">unix:/run/selfpost/journal.sock</span></td><td><span class="tag is-success is-light">ok</span></td></tr>
|
||||||
<tr><td>sender-check<br><span class="sp-muted sp-mono sp-small">unix:/run/selfpost/sender.sock</span></td><td><span class="tag is-success is-light">ok</span></td></tr>
|
<tr><td>sender-check<br><span class="sp-muted sp-mono sp-small">unix:/run/selfpost/sender.sock</span></td><td><span class="tag is-success is-light">ok</span></td></tr>
|
||||||
|
<tr><td>inbound-journal-milter <span class="sp-muted">· Inbound log</span><br><span class="sp-muted sp-mono sp-small">unix:/run/selfpost/inbound-journal.sock</span></td><td><span class="tag is-success is-light">ok</span></td></tr>
|
||||||
<tr><td>rspamd <span class="sp-muted">· inbound spam filter</span><br><span class="sp-muted sp-mono sp-small">inet:antispam:11332</span></td><td><span class="tag is-success is-light">ok</span></td></tr>
|
<tr><td>rspamd <span class="sp-muted">· inbound spam filter</span><br><span class="sp-muted sp-mono sp-small">inet:antispam:11332</span></td><td><span class="tag is-success is-light">ok</span></td></tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
</table></div>
|
</table></div>
|
||||||
@@ -141,10 +143,10 @@
|
|||||||
<div class="sp-box-head"><span class="sp-no">04</span><h2>TLS certificate</h2><div class="sp-end"><span class="tag is-success is-light">ok</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-head"><span class="sp-no">04</span><h2>TLS certificate</h2><div class="sp-end"><span class="tag is-success is-light">ok</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-11-02 09:14 UTC</dd></div>
|
<div><dt>Expires</dt><dd class="sp-mono">2026-11-02 09:14</dd></div>
|
||||||
<div><dt>Names</dt><dd class="sp-mono">mail.example.org</dd></div>
|
<div><dt>Names <span class="tag is-success is-light">matches</span></dt><dd class="sp-mono">mail.example.org</dd></div>
|
||||||
<div><dt>Renewal expires</dt><dd class="sp-mono">2027-01-31 08:02 UTC</dd></div>
|
<div><dt>Renewal expires</dt><dd class="sp-mono">2027-01-31 08:02</dd></div>
|
||||||
<div><dt>Renewal mounted</dt><dd class="sp-mono">2026-10-15 03:12 UTC</dd></div>
|
<div><dt>Renewal mounted</dt><dd class="sp-mono">2026-10-15 03:12</dd></div>
|
||||||
</dl>
|
</dl>
|
||||||
<p class="mt-4 sp-muted">Postfix still serves the certificate above. A renewed one is waiting in the mounted file; Postfix takes it up at its next check for a renewal, at most a day after it was mounted.</p>
|
<p class="mt-4 sp-muted">Postfix still serves the certificate above. A renewed one is waiting in the mounted file; Postfix takes it up at its next check for a renewal, at most a day after it was mounted.</p>
|
||||||
</div>
|
</div>
|
||||||
@@ -165,14 +167,30 @@
|
|||||||
<div class="box" id="configuration" data-order="8">
|
<div class="box" id="configuration" data-order="8">
|
||||||
<div class="sp-box-head"><span class="sp-no">08</span><h2>Configuration</h2><div class="sp-end">Safe to run at any time</div></div>
|
<div class="sp-box-head"><span class="sp-no">08</span><h2>Configuration</h2><div class="sp-end">Safe to run at any time</div></div>
|
||||||
<div class="sp-box-body">
|
<div class="sp-box-body">
|
||||||
|
<div class="mb-3"><dl class="sp-facts">
|
||||||
|
<div><dt>Your address</dt><dd class="sp-mono">198.51.100.7</dd><dd class="sp-muted">From X-Forwarded-For, which the trusted proxy 172.18.0.1 set</dd></div>
|
||||||
|
</dl></div>
|
||||||
<p class="mb-3">Regenerates the Postfix configuration and the spam filter's lists from the database, removes the keys of deleted domains, and reloads Postfix. Use it after editing the files by hand, after restoring a backup, or when the running configuration looks out of step with the domain and application lists. It does not touch the queue or the TLS certificate.</p>
|
<p class="mb-3">Regenerates the Postfix configuration and the spam filter's lists from the database, removes the keys of deleted domains, and reloads Postfix. Use it after editing the files by hand, after restoring a backup, or when the running configuration looks out of step with the domain and application lists. It does not touch the queue or the TLS certificate.</p>
|
||||||
<form method="post" action="#"><button type="submit" class="button"><i class="ti ti-reload"></i>Reload configuration</button></form>
|
<form method="post" action="#"><button type="submit" class="button"><i class="ti ti-reload"></i>Reload configuration</button></form>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="box" id="test-message" data-order="9">
|
||||||
|
<div class="sp-box-head"><span class="sp-no">09</span><h2>Test message</h2><div class="sp-end">Shows what the receiving server answers</div><a class="sp-help" href="help.html#test-message" title="What the test proves"><i class="ti ti-help-circle"></i></a></div>
|
||||||
|
<div class="sp-box-body">
|
||||||
|
<form class="sp-form" method="post" action="out-message.html">
|
||||||
|
<div class="field"><label class="label" for="rcpt">Recipient</label>
|
||||||
|
<div class="field has-addons">
|
||||||
|
<div class="control is-expanded has-icons-left"><input class="input" id="rcpt" name="recipient" type="email" placeholder="you@elsewhere.example" required><span class="icon is-left"><i class="ti ti-mail"></i></span></div>
|
||||||
|
<div class="control"><button type="submit" class="button is-primary"><i class="ti ti-send"></i>Send</button></div>
|
||||||
|
</div>
|
||||||
|
<p class="help">From <span class="sp-mono">postmaster@mail.example.org</span>, signed with the server's own key, through Postfix as its notifications go. It opens in the <a href="out-log.html">Outbound log</a>, where its page follows it to the receiving server. Ten an hour.</p></div>
|
||||||
|
</form>
|
||||||
|
</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 · Copyright © 2026 Mikhail Yenuchenko · <a href="#">License (AGPL-3.0)</a> · <a href="#">Source</a> · No warranty</footer>
|
||||||
</div>
|
</div>
|
||||||
</main>
|
</main>
|
||||||
</body>
|
</body>
|
||||||
|
|||||||
@@ -12,8 +12,8 @@
|
|||||||
<body>
|
<body>
|
||||||
<nav class="navbar is-primary" aria-label="main navigation">
|
<nav class="navbar is-primary" aria-label="main navigation">
|
||||||
<div class="container">
|
<div class="container">
|
||||||
<div class="navbar-brand"><a class="navbar-item" href="overview.html"><img src="../../selfpost-wordmark.svg" alt="SelfPost"></a></div>
|
<div class="navbar-brand"><a class="navbar-item" href="overview.html"><img src="../../selfpost-wordmark.svg" alt="SelfPost"></a><details class="sp-touchnav"><summary>Menu</summary><div class="sp-touchnav-panel"><a href="overview.html">Overview</a><details class="sp-touchnav-group"><summary>Outbound</summary><a href="out-domains.html"><i class="ti ti-world-upload"></i>Domains</a><a href="out-log.html"><i class="ti ti-list-details"></i>Log</a><a href="out-queue.html"><i class="ti ti-stack-2"></i>Queue</a><a href="dmarc.html"><i class="ti ti-report-analytics"></i>DMARC reports</a></details><details class="sp-touchnav-group"><summary>Inbound</summary><a href="in-domains.html"><i class="ti ti-world-download"></i>Domains</a><a href="in-log.html"><i class="ti ti-list-details"></i>Log</a><a href="in-queue.html"><i class="ti ti-stack-2"></i>Queue</a><a href="in-quarantine.html"><i class="ti ti-mail-pause"></i>Quarantine</a><a href="in-filter-lists.html"><i class="ti ti-filter"></i>Filter lists</a></details><details class="sp-touchnav-group"><summary>Server</summary><a href="health.html"><i class="ti ti-heartbeat"></i>Health</a><a href="preflight.html"><i class="ti ti-checklist"></i>Preflight</a><a href="system-log.html"><i class="ti ti-file-text"></i>System log</a><a href="backup.html"><i class="ti ti-archive"></i>Backup</a><a href="users.html"><i class="ti ti-users"></i>Users</a><a href="settings.html"><i class="ti ti-settings"></i>Settings</a></details><details class="sp-touchnav-group" open><summary><i class="ti ti-user-circle"></i>admin</summary><a href="account.html"><i class="ti ti-key"></i>Account</a><a href="help.html"><i class="ti ti-help"></i>Help</a><form method="post" action="login.html"><button type="submit"><i class="ti ti-logout"></i>Sign out</button></form></details></div></details></div>
|
||||||
<div class="navbar-menu is-active">
|
<div class="navbar-menu">
|
||||||
<div class="navbar-start">
|
<div class="navbar-start">
|
||||||
<a class="navbar-item" href="overview.html">Overview</a>
|
<a class="navbar-item" href="overview.html">Overview</a>
|
||||||
<div class="navbar-item has-dropdown is-hoverable">
|
<div class="navbar-item has-dropdown is-hoverable">
|
||||||
@@ -80,6 +80,7 @@
|
|||||||
<ul class="menu-list">
|
<ul class="menu-list">
|
||||||
<li><a class="is-active" href="#checks"><i class="ti ti-activity-heartbeat"></i>Health checks</a></li>
|
<li><a class="is-active" href="#checks"><i class="ti ti-activity-heartbeat"></i>Health checks</a></li>
|
||||||
<li><a href="#certificate"><i class="ti ti-certificate"></i>TLS certificate</a></li>
|
<li><a href="#certificate"><i class="ti ti-certificate"></i>TLS certificate</a></li>
|
||||||
|
<li><a href="#test-message"><i class="ti ti-send"></i>Test message</a></li>
|
||||||
<li><a href="#rdns"><i class="ti ti-arrows-exchange"></i>Reverse DNS</a></li>
|
<li><a href="#rdns"><i class="ti ti-arrows-exchange"></i>Reverse DNS</a></li>
|
||||||
</ul>
|
</ul>
|
||||||
<p class="menu-label">Outbound</p>
|
<p class="menu-label">Outbound</p>
|
||||||
@@ -117,19 +118,20 @@
|
|||||||
<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, 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="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. <em>Your address</em>, in the Configuration card, is the address the panel sees your browser at: behind a reverse proxy it must be your own, taken from <code>X-Forwarded-For</code>; when it is the proxy's, every visitor shares one sign-in limit, and Health warns and names the address to put in <code>TRUSTED_PROXY_CIDR</code>.</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. Postfix serves its own copy of the mounted file and looks for a renewed one once a day by default; until it takes one up, the card shows it as a renewal waiting, and warns once it has waited longer than that check allows - the container log's cert-reload lines say why. It warns 14 days before expiry unless a renewal is already waiting, 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="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. Postfix serves its own copy of the mounted file and looks for a renewed one once a day by default; until it takes one up, the card shows it as a renewal waiting, and warns once it has waited longer than that check allows - the container log's cert-reload lines say why. It warns 14 days before expiry unless a renewal is already waiting, and fails once expired, or when the file is missing or unreadable — clients that verify certificates then refuse to connect. It also fails when none of its names is the hostname (<code>SELFPOST_HOSTNAME</code>) - the tag beside <em>Names</em> says which: a client that verifies the name refuses it however valid it is.</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 class="sp-help-topic" id="test-message"><h3>Test message</h3><p>A message from <code>postmaster@</code> the server's hostname to one address you type, sent the way notifications are: through Postfix, signed with the DKIM key of the server's own name. It proves the path from this server - TLS, its address's reputation, the SPF and DKIM records of its name - not your domains', which only mail from an application does. The page that opens is its row in the Outbound log: it shows whether the receiving server took it, put it off or refused it, with the lines of <code>mail.log</code>. Whether it then reached the inbox or the spam folder only the mailbox can say. The global role sends it, at most ten an hour.</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. <em>Greeting</em> is the line Postfix opens every connection with, <code>220</code> and its banner; it must name the hostname too - a receiver compares it with the name it looked up - and a container id or <code>localhost</code> there is a warning.</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. 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="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. A record DNS already holds as shown is folded to its name and status; Show values opens it to copy them again. The SPF check is shallow: it looks for this server's address in the record itself and does not follow <code>include:</code> or <code>redirect=</code>, so a record that relies on them is reported as a warning, not a failure. <code>p=none</code> does not affect delivery; tighten to <code>quarantine</code>, then <code>reject</code>, once the reports look clean. A domain's report address is set in its Domain settings.</p></div>
|
||||||
<div class="sp-help-topic" id="apps"><h3>Applications</h3><p>An application is one SASL login, unique across all domains. It may send as any address of its domain or only as listed addresses, and, with a list of client IPs set, only from those addresses. The password is shown once at creation or regeneration and is not stored in readable form.</p></div>
|
<div class="sp-help-topic" id="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 the panel does not show it again; the server keeps it, so a domain export can carry it.</p></div>
|
||||||
<div class="sp-help-topic" id="sender-check"><h3>Sender check</h3><p>An application sends only as itself, and that holds for both senders a message has. The envelope sender is held against the login by Postfix. The <code>From:</code> line, the one a mail program shows and the one DMARC holds the DKIM signature to, is held against it by the panel, under the same rule: an application set to any address of its domain may write any address of that domain, and one with a list of addresses only those. A message with any other <code>From:</code> - another of your domains, the server's own name, an address somewhere else, or none at all - is refused for good with <code>550 5.7.1</code>. The line must hold one address, read as a mail program reads it: what a display name around it says is not judged, and a line holding more than one address is refused. It is a rejected row of the Outbound log, and its page says which address was refused. This is what keeps one application from sending, under this server's signature, as another domain or another application.<br><br>The check is strict. While the panel does not answer, Postfix does not accept mail unchecked: it replies with a temporary error and the application sends again later. Server › Health counts what that costs: for how long the panel did not answer while Postfix itself was running; how many connections Postfix turned away because of it - it refuses from the connection on, before a message is offered, so a client that tries three times is three and a monitor that only connects is one; and how many messages were refused with a temporary error in the middle, when the panel stopped answering after a message had begun. A time marked ≈ includes a start at which supervisord could not say since when Postfix was running: the time since the panel's last sign of life is counted, which may be more than Postfix was up. The card warns when a connection was turned away or a message refused in the last 24 hours. If the numbers stay at nothing, the check is where it should be; if they do not, it belongs in a process of its own.</p></div>
|
<div class="sp-help-topic" id="sender-check"><h3>Sender check</h3><p>An application sends only as itself, and that holds for every sender a message names. The envelope sender is held against the login by Postfix. The <code>From:</code> line, the one a mail program shows and the one DMARC holds the DKIM signature to, is held against it by the panel, under the same rule: an application set to any address of its domain may write any address of that domain, and one with a list of addresses only those. A message with any other <code>From:</code> - another of your domains, the server's own name, an address somewhere else, or none at all - is refused for good with <code>550 5.7.1</code>. The line must hold one address, read as a mail program reads it: what a display name around it says is not judged, and a line holding more than one address is refused. A <code>Sender:</code> line, which a mail program shows beside <code>From:</code> as "on behalf of", is held to the same rule when the message has one. A system that writes for people outside your domains writes its own address in <code>From:</code> with the person's name, and the person's address in <code>Reply-To:</code>: their domain in <code>From:</code> would fail DMARC at the receiver anyway. It is a rejected row of the Outbound log, and its page says which address was refused. This is what keeps one application from sending, under this server's signature, as another domain or another application.<br><br>The check is strict. While the panel does not answer, Postfix does not accept mail unchecked: it replies with a temporary error and the application sends again later. Server › Health counts what that costs: for how long the panel did not answer while Postfix itself was running; how many connections Postfix turned away because of it - it refuses from the connection on, before a message is offered, so a client that tries three times is three and a monitor that only connects is one; and how many messages were refused with a temporary error in the middle, when the panel stopped answering after a message had begun. A time marked ≈ includes a start at which supervisord could not say since when Postfix was running: the time since the panel's last sign of life is counted, which may be more than Postfix was up. The card warns when a connection was turned away or a message refused in the last 24 hours. If the numbers stay at nothing, the check is where it should be; if they do not, it belongs in a process of its own.</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="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; or closed by the panel when Postfix no longer held the message and <code>mail.log</code> never said what became of it) and rejected (refused before queueing, so it never reached the queue: under a level-2 rate limit, from a client address the application does not allow, or for a <code>From:</code> line that is not the application's - the message's page says which). A message's page lists these steps and the lines Postfix wrote about it in <code>mail.log</code>, found by queue id - and for a message refused before it had one, Postfix's one line about the refusal. The lines are kept with the row: they stay on the page after that file has rotated, for as long as the row does. A message accepted before version 2.2.0 shows them only while they are still in the current file. Rows are deleted after the retention set in Server › Settings, 90 days by default, and their lines with them. A domain administrator sees only the rows of their own domains.</p></div>
|
<div class="sp-help-topic" id="log"><h3>Outbound log</h3><p>One row per message and recipient (a message refused for its From: or Sender: line is one row, however many recipients it had), 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; or closed by the panel when Postfix no longer held the message and <code>mail.log</code> never said what became of it) and rejected (refused before queueing, so it never reached the queue: under a level-2 rate limit, from a client address the application does not allow, or for a <code>From:</code> or <code>Sender:</code> line that is not the application's - the message's page says which). A message's page lists these steps and the lines Postfix wrote about it in <code>mail.log</code>, found by queue id - and for a message refused before it had one, Postfix's one line about the refusal. The lines are kept with the row: they stay on the page after that file has rotated, for as long as the row does. A message accepted before version 2.2.0 shows them only while they are still in the current file. Rows are deleted after the retention set in Server › Settings, 90 days by default, and their lines with them. 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="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 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 - its day is the reporter's, counted in UTC, and that is the day the lists and a report's title name, while the report's page gives its window in this panel's zone: 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>
|
||||||
<div class="box">
|
<div class="box">
|
||||||
<div class="sp-box-head"><span class="sp-no">03</span><h2>Inbound</h2></div>
|
<div class="sp-box-head"><span class="sp-no">03</span><h2>Inbound</h2></div>
|
||||||
@@ -137,24 +139,24 @@
|
|||||||
<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 sends only over TLS, and the mail waits in the queue while the upstream does not offer it or the TLS handshake fails - an upstream that answers STARTTLS with a permanent error bounces it; 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="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 sends only over TLS, and the mail waits in the queue while the upstream does not offer it or the TLS handshake fails - an upstream that answers STARTTLS with a permanent error bounces it; 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="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 refused with a temporary error, so the sending server tries again later, as the server's setting says; the box states which.</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 refused with a temporary error, so the sending server tries again later, as the server's setting says; the box states which.</p></div>
|
||||||
<div class="sp-help-topic" id="inbound-log"><h3>Inbound log</h3><p>One row per recipient of a message offered on port 25 for an inbound domain, with what was decided: <span class="tag is-success is-light">accept</span> — handed on to the upstream; <span class="tag is-danger is-light">reject</span> — refused for good; <span class="tag is-warning is-light">tempfail</span> — the sender was asked to come back later; <span class="tag is-primary is-light">quarantine</span> — held by the filter. <em>Why</em> names who decided. <code>rspamd</code> is the spam filter, with its action, the score against the reject threshold and the heaviest symbols; <code>postfix</code> is the relay's own rule — an unknown recipient, a sender domain that does not exist — with its reply; <code>selfpost</code> means nobody judged the message: it was relayed with no filter attached. Accept says what this server did, not what the upstream did with the message afterwards.<br><br>Accepted mail is recorded as it passes; refused mail is read from <code>mail.log</code>, so its row has no subject, and a message refused after its text was sent is one row however many recipients it had. A refusal to relay for a domain that is not an inbound one is every scanner on the internet and is not recorded. Rows are kept for the number of days set in Settings, and never more than 10 000.</p></div>
|
<div class="sp-help-topic" id="inbound-log"><h3>Inbound log</h3><p>One row per recipient of a message offered on port 25 for an inbound domain, with what was decided: <span class="tag is-success is-light">accept</span> — taken for the upstream; <span class="tag is-danger is-light">reject</span> — refused for good; <span class="tag is-warning is-light">tempfail</span> — the sender was asked to come back later; <span class="tag is-primary is-light">quarantine</span> — held by the filter. <em>Why</em> names who decided. <code>rspamd</code> is the spam filter, with its action, the score against the reject threshold and the heaviest symbols; <code>postfix</code> is the relay's own rule — an unknown recipient, a sender domain that does not exist — with its reply; <code>selfpost</code> means nobody judged the message: it was relayed with no filter attached. <em>Upstream</em> says what became of an accepted message: <span class="tag is-light">queued</span> — not tried yet; <span class="tag is-primary is-light">held</span> — held in the queue by the filter, not tried until it is released; <span class="tag is-success is-light">sent</span> — the upstream took it; <span class="tag is-warning is-light">deferred</span> — the upstream put it off or could not be reached, and Postfix tries again; <span class="tag is-danger is-light">bounced</span> — the upstream refused it, or it waited in the queue too long, and the sender was sent a bounce, if it had a return address. It opens the last reply and the host that gave it. A refused message has no upstream result, and neither has a row logged before 2.10.0.<br><br>Accepted mail is recorded as it passes; refused mail is read from <code>mail.log</code>, so its row has no subject, and a message refused after its text was sent is one row however many recipients it had. A refusal to relay for a domain that is not an inbound one is every scanner on the internet and is not recorded. Rows are kept for the number of days set in Settings, and never more than 10 000.</p></div>
|
||||||
<div class="sp-help-topic" id="filter-lists"><h3>Filter lists</h3><p>Two lists for the whole server that correct the spam filter by hand. An entry is a sender address, a sender domain — that domain, not its subdomains — or a client address or network in CIDR form. A sender on the Deny list is refused outright. A sender on the Allow list is given a weight no spam score outweighs, so the filter lets it through. The sender's address is whatever the sending server says it is - anyone can claim to be a sender you allow - so allow a client address or network where you can, and a sender address only for mail you would rather receive forged than lose. When entries of both lists match one message, <strong>Deny wins</strong>.<br><br>SelfPost does not apply the lists itself: it writes them as files under <code>/data/antispam/</code>, and rspamd rereads a file within seconds of a change. With the spam filter off the lists are kept and can be edited, but nothing reads them. The sender matched is the envelope sender — the one in the log — not the From line a mail program shows. Matched counts the messages in the Inbound log that the entry decided, so it reaches back as far as the log does; never means not once in that time, and such an entry is one to consider removing.</p></div>
|
<div class="sp-help-topic" id="filter-lists"><h3>Filter lists</h3><p>Two lists for the whole server that correct the spam filter by hand. An entry is a sender address, a sender domain — that domain, not its subdomains — or a client address or network in CIDR form. A sender on the Deny list is refused outright. A sender on the Allow list is given a weight no spam score outweighs, so the filter lets it through. The sender's address is whatever the sending server says it is - anyone can claim to be a sender you allow - so allow a client address or network where you can, and a sender address only for mail you would rather receive forged than lose. When entries of both lists match one message, <strong>Deny wins</strong>.<br><br>SelfPost does not apply the lists itself: it writes them as files under <code>/data/antispam/</code>, and rspamd rereads a file within seconds of a change. With the spam filter off the lists are kept and can be edited, but nothing reads them. The sender matched is the envelope sender — the one in the log — not the From line a mail program shows. Matched counts the messages in the Inbound log that the entry decided, so it reaches back as far as the log does; never means not once in that time, and such an entry is one to consider removing.</p></div>
|
||||||
</div>
|
</div>
|
||||||
<div class="box">
|
<div class="box">
|
||||||
<div class="sp-box-head"><span class="sp-no">04</span><h2>Server</h2></div>
|
<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 and application credentials), <code>docker-compose.yml</code>, <code>.env</code> and <code>certs/</code>. The Postfix queue is not in it. The backup is taken while the server is running, and Postfix keeps moving messages through the queue, so a copy could hold a message twice or miss it; mail still waiting for delivery is therefore not carried over, and a restored server starts with an empty queue. To keep the queue, stop the container and archive the project directory instead. The private key in <code>certs/</code> has to be readable by the panel: give it to group 10998 on the host (<code>chgrp 10998</code>, <code>chmod 640</code>). Until then the panel refuses to make a backup and names the file, rather than hand over an archive without the key. 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="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 and application credentials), <code>docker-compose.yml</code>, <code>.env</code> and <code>certs/</code>. Both it and a domain's export ask for your account password - the one you sign in with - at every download, so a session left open is not enough to take them; every attempt, right or wrong, counts against the limit of a password change, ten in 15 minutes. The Postfix queue is not in it. The backup is taken while the server is running, and Postfix keeps moving messages through the queue, so a copy could hold a message twice or miss it; mail still waiting for delivery is therefore not carried over, and a restored server starts with an empty queue. To keep the queue, stop the container and archive the project directory instead. The private key in <code>certs/</code> has to be readable by the panel: give it to group 10998 on the host (<code>chgrp 10998</code>, <code>chmod 640</code>). Until then the panel refuses to make a backup and names the file, rather than hand over an archive without the key. 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="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 everywhere; set for yourself, it keeps the session you set it from.</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. With the inbound relay on there is a second number, for the Inbound log, with the same range. 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. Notifications are turned on or off here for everyone.</p></div>
|
<div class="sp-help-topic" id="settings"><h3>Settings</h3><p>Settings holds what is true of the whole instance. The time zone is the container's, <code>TZ</code> in <code>.env</code> (UTC when it is not set): every time the panel shows and every day it counts is that zone's, as are the stamps Postfix writes in <code>mail.log</code>, so the two always agree; changing it is a restart of the container. The one exception is the day a DMARC report covers, which is the reporter's, counted in UTC. 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. With the inbound relay on there is a second number, for the Inbound log, with the same range. 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. Notifications are turned on or off here for everyone.</p></div>
|
||||||
<div class="sp-help-topic" id="notifications"><h3>Notifications</h3><p>The panel writes to its users when something needs them, from <code>postmaster@</code> the server's hostname. Four things are reported. A sending domain's SPF, DKIM or DMARC record stops matching: sent once two hourly checks in a row have found it so, and once more when the record matches again. A level-2 rate limit refuses mail: sent at the first refusal, then at most once a day for the same limit. The TLS certificate is 14 days from expiry, and again at 3 days if it is still the same certificate. A message has waited in the queue for more than three days, with one more message when none is left. A global user is told all of it; a domain administrator is told about the DNS records and limits of their own domains.<br><br>Each user turns notifications on or off under Account and needs an e-mail there; Server › Settings turns them off for everyone. They are the panel's own mail: not rows of the Outbound log, and not counted against any limit.<br><br>For that mail to arrive, the server's own name needs two DNS records of its own, as each of your domains does: a DKIM key, which the panel generates and signs notifications with, and an SPF record, <code>v=spf1 a -all</code>. Server › Settings gives both with the values to publish, and Server › Health shows whether they are in place. Without them notifications are still sent, and a receiver may file them as spam. Mail of your domains does not depend on either.</p></div>
|
<div class="sp-help-topic" id="notifications"><h3>Notifications</h3><p>The panel writes to its users when something needs them, from <code>postmaster@</code> the server's hostname. Four things are reported. A sending domain's SPF, DKIM or DMARC record stops matching: sent once two hourly checks in a row have found it so, and once more when the record matches again. A level-2 rate limit refuses mail: sent at the first refusal, then at most once a day for the same sending domain. The TLS certificate is 14 days from expiry, and again at 3 days if it is still the same certificate. A message has waited in the queue for more than three days, with one more message when none is left. A global user is told all of it; a domain administrator is told about the DNS records and limits of their own domains.<br><br>Each user turns notifications on or off under Account and needs an e-mail there; Server › Settings turns them off for everyone. They are the panel's own mail: not rows of the Outbound log, and not counted against any limit.<br><br>For that mail to arrive, the server's own name needs two DNS records of its own, as each of your domains does: a DKIM key, which the panel generates and signs notifications with, and an SPF record, <code>v=spf1 a -all</code>. Server › Settings gives both with the values to publish and whether they are in place, and its Re-check DNS checks them again once they are published; Server › Health shows them too. Without them notifications are still sent, and a receiver may file them as spam. Mail of your domains does not depend on either.</p></div>
|
||||||
</div>
|
</div>
|
||||||
<div class="box">
|
<div class="box">
|
||||||
<div class="sp-box-head"><span class="sp-no">05</span><h2>Account</h2></div>
|
<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, whether the panel sends you notifications, 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 class="sp-help-topic" id="account"><h3>Your account</h3><p>Account is your own, whatever your role: your username and e-mail, whether the panel sends you notifications, 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. A new e-mail is sent a link that confirms it, which works once, for 24 hours; changing the e-mail asks for your password. Notifications go to the address whether it is confirmed or not, but only a confirmed one can get a link to reset a forgotten password: <em>Forgot your password?</em> on the sign-in page sends it, for 30 minutes and once, and setting the new password signs every session of the account out. Without a confirmed e-mail the global administrator sets a new password under Users, and the only global administrator recovers from the container.</p></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 · Copyright © 2026 Mikhail Yenuchenko · <a href="#">License (AGPL-3.0)</a> · <a href="#">Source</a> · No warranty</footer>
|
||||||
</div>
|
</div>
|
||||||
</main>
|
</main>
|
||||||
</body>
|
</body>
|
||||||
|
|||||||
@@ -12,8 +12,8 @@
|
|||||||
<body>
|
<body>
|
||||||
<nav class="navbar is-primary" aria-label="main navigation">
|
<nav class="navbar is-primary" aria-label="main navigation">
|
||||||
<div class="container">
|
<div class="container">
|
||||||
<div class="navbar-brand"><a class="navbar-item" href="overview.html"><img src="../../selfpost-wordmark.svg" alt="SelfPost"></a></div>
|
<div class="navbar-brand"><a class="navbar-item" href="overview.html"><img src="../../selfpost-wordmark.svg" alt="SelfPost"></a><details class="sp-touchnav"><summary>Menu</summary><div class="sp-touchnav-panel"><a href="overview.html">Overview</a><details class="sp-touchnav-group"><summary>Outbound</summary><a href="out-domains.html"><i class="ti ti-world-upload"></i>Domains</a><a href="out-log.html"><i class="ti ti-list-details"></i>Log</a><a href="out-queue.html"><i class="ti ti-stack-2"></i>Queue</a><a href="dmarc.html"><i class="ti ti-report-analytics"></i>DMARC reports</a></details><details class="sp-touchnav-group" open><summary>Inbound</summary><a href="in-domains.html" aria-current="page"><i class="ti ti-world-download"></i>Domains</a><a href="in-log.html"><i class="ti ti-list-details"></i>Log</a><a href="in-queue.html"><i class="ti ti-stack-2"></i>Queue</a><a href="in-quarantine.html"><i class="ti ti-mail-pause"></i>Quarantine</a><a href="in-filter-lists.html"><i class="ti ti-filter"></i>Filter lists</a></details><details class="sp-touchnav-group"><summary>Server</summary><a href="health.html"><i class="ti ti-heartbeat"></i>Health</a><a href="preflight.html"><i class="ti ti-checklist"></i>Preflight</a><a href="system-log.html"><i class="ti ti-file-text"></i>System log</a><a href="backup.html"><i class="ti ti-archive"></i>Backup</a><a href="users.html"><i class="ti ti-users"></i>Users</a><a href="settings.html"><i class="ti ti-settings"></i>Settings</a></details><details class="sp-touchnav-group"><summary><i class="ti ti-user-circle"></i>admin</summary><a href="account.html"><i class="ti ti-key"></i>Account</a><a href="help.html"><i class="ti ti-help"></i>Help</a><form method="post" action="login.html"><button type="submit"><i class="ti ti-logout"></i>Sign out</button></form></details></div></details></div>
|
||||||
<div class="navbar-menu is-active">
|
<div class="navbar-menu">
|
||||||
<div class="navbar-start">
|
<div class="navbar-start">
|
||||||
<a class="navbar-item" href="overview.html">Overview</a>
|
<a class="navbar-item" href="overview.html">Overview</a>
|
||||||
<div class="navbar-item has-dropdown is-hoverable">
|
<div class="navbar-item has-dropdown is-hoverable">
|
||||||
@@ -94,7 +94,7 @@
|
|||||||
</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 · Copyright © 2026 Mikhail Yenuchenko · <a href="#">License (AGPL-3.0)</a> · <a href="#">Source</a> · No warranty</footer>
|
||||||
</div>
|
</div>
|
||||||
</main>
|
</main>
|
||||||
</body>
|
</body>
|
||||||
|
|||||||
@@ -12,8 +12,8 @@
|
|||||||
<body>
|
<body>
|
||||||
<nav class="navbar is-primary" aria-label="main navigation">
|
<nav class="navbar is-primary" aria-label="main navigation">
|
||||||
<div class="container">
|
<div class="container">
|
||||||
<div class="navbar-brand"><a class="navbar-item" href="overview.html"><img src="../../selfpost-wordmark.svg" alt="SelfPost"></a></div>
|
<div class="navbar-brand"><a class="navbar-item" href="overview.html"><img src="../../selfpost-wordmark.svg" alt="SelfPost"></a><details class="sp-touchnav"><summary>Menu</summary><div class="sp-touchnav-panel"><a href="overview.html">Overview</a><details class="sp-touchnav-group"><summary>Outbound</summary><a href="out-domains.html"><i class="ti ti-world-upload"></i>Domains</a><a href="out-log.html"><i class="ti ti-list-details"></i>Log</a><a href="out-queue.html"><i class="ti ti-stack-2"></i>Queue</a><a href="dmarc.html"><i class="ti ti-report-analytics"></i>DMARC reports</a></details><details class="sp-touchnav-group" open><summary>Inbound</summary><a href="in-domains.html" aria-current="page"><i class="ti ti-world-download"></i>Domains</a><a href="in-log.html"><i class="ti ti-list-details"></i>Log</a><a href="in-queue.html"><i class="ti ti-stack-2"></i>Queue</a><a href="in-quarantine.html"><i class="ti ti-mail-pause"></i>Quarantine</a><a href="in-filter-lists.html"><i class="ti ti-filter"></i>Filter lists</a></details><details class="sp-touchnav-group"><summary>Server</summary><a href="health.html"><i class="ti ti-heartbeat"></i>Health</a><a href="preflight.html"><i class="ti ti-checklist"></i>Preflight</a><a href="system-log.html"><i class="ti ti-file-text"></i>System log</a><a href="backup.html"><i class="ti ti-archive"></i>Backup</a><a href="users.html"><i class="ti ti-users"></i>Users</a><a href="settings.html"><i class="ti ti-settings"></i>Settings</a></details><details class="sp-touchnav-group"><summary><i class="ti ti-user-circle"></i>admin</summary><a href="account.html"><i class="ti ti-key"></i>Account</a><a href="help.html"><i class="ti ti-help"></i>Help</a><form method="post" action="login.html"><button type="submit"><i class="ti ti-logout"></i>Sign out</button></form></details></div></details></div>
|
||||||
<div class="navbar-menu is-active">
|
<div class="navbar-menu">
|
||||||
<div class="navbar-start">
|
<div class="navbar-start">
|
||||||
<a class="navbar-item" href="overview.html">Overview</a>
|
<a class="navbar-item" href="overview.html">Overview</a>
|
||||||
<div class="navbar-item has-dropdown is-hoverable">
|
<div class="navbar-item has-dropdown is-hoverable">
|
||||||
@@ -101,8 +101,8 @@
|
|||||||
<div class="column is-9">
|
<div class="column is-9">
|
||||||
<div class="box" id="dns">
|
<div class="box" id="dns">
|
||||||
<div class="sp-box-head"><span class="sp-no">01</span><h2>MX record</h2><div class="sp-end">Checked 1 min ago</div><a class="sp-help" href="help.html#inbound" title="Backup-MX and forwarding"><i class="ti ti-help-circle"></i></a></div>
|
<div class="sp-box-head"><span class="sp-no">01</span><h2>MX record</h2><div class="sp-end">Checked 1 min ago</div><a class="sp-help" href="help.html#inbound" title="Backup-MX and forwarding"><i class="ti ti-help-circle"></i></a></div>
|
||||||
<div class="sp-record">
|
<details class="sp-record">
|
||||||
<div class="sp-record-head"><h3>MX</h3><span class="tag is-success is-light">published</span><small>Keep any existing primary MX if this is a backup-MX</small></div>
|
<summary class="sp-record-head"><h3>MX</h3><span class="tag is-success is-light">published</span><small>Keep any existing primary MX if this is a backup-MX</small><span class="sp-record-toggle"><i class="ti ti-eye"></i><span>Show values</span><span>Hide values</span></span></summary>
|
||||||
<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="lists.example.org"></div><div class="control"><button type="button" class="button"><i class="ti ti-copy"></i>Copy</button></div></div></div>
|
<div class="field has-addons"><div class="control is-expanded"><input class="input" readonly value="lists.example.org"></div><div class="control"><button type="button" class="button"><i class="ti ti-copy"></i>Copy</button></div></div></div>
|
||||||
@@ -110,7 +110,7 @@
|
|||||||
</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="10 mail.example.org."></div><div class="control"><button type="button" class="button"><i class="ti ti-copy"></i>Copy</button></div></div></div>
|
<div class="field has-addons"><div class="control is-expanded"><input class="input" readonly value="10 mail.example.org."></div><div class="control"><button type="button" class="button"><i class="ti ti-copy"></i>Copy</button></div></div></div>
|
||||||
</div>
|
</details>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="columns">
|
<div class="columns">
|
||||||
@@ -142,7 +142,7 @@
|
|||||||
dev@lists.example.org
|
dev@lists.example.org
|
||||||
owner@lists.example.org
|
owner@lists.example.org
|
||||||
security@lists.example.org</textarea></div>
|
security@lists.example.org</textarea></div>
|
||||||
<p class="help">Unknown recipients are rejected at RCPT, so this relay generates no backscatter. Prefer a list unless the upstream itself rejects unknowns.</p></div>
|
<p class="help">One per line or comma-separated. Unknown recipients are rejected at RCPT, so this relay generates no backscatter. Prefer a list unless the upstream itself rejects unknowns.</p></div>
|
||||||
<button type="submit" class="button is-primary">Save recipients</button>
|
<button type="submit" class="button is-primary">Save recipients</button>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
@@ -154,17 +154,16 @@ security@lists.example.org</textarea></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-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>Applies to</dt><dd>Every inbound domain of this server</dd></div>
|
||||||
<div><dt>Suspicious mail</dt><dd>Held — <a href="in-quarantine.html">1 message in quarantine</a> for this domain</dd></div>
|
<div><dt>Filter</dt><dd class="sp-mono">inet:antispam:11332</dd></div>
|
||||||
<div><dt>Spam</dt><dd>Refused at the door; see <em>reject</em> in the <a href="in-log.html">log</a></dd></div>
|
<div><dt>If the filter is down</dt><dd>Mail is accepted unfiltered</dd></div>
|
||||||
<div><dt>Allow and deny lists</dt><dd class="sp-muted">Kept by the global administrator, one pair for the whole server</dd></div>
|
|
||||||
</dl>
|
</dl>
|
||||||
</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 · Copyright © 2026 Mikhail Yenuchenko · <a href="#">License (AGPL-3.0)</a> · <a href="#">Source</a> · No warranty</footer>
|
||||||
</div>
|
</div>
|
||||||
</main>
|
</main>
|
||||||
</body>
|
</body>
|
||||||
|
|||||||
@@ -12,8 +12,8 @@
|
|||||||
<body>
|
<body>
|
||||||
<nav class="navbar is-primary" aria-label="main navigation">
|
<nav class="navbar is-primary" aria-label="main navigation">
|
||||||
<div class="container">
|
<div class="container">
|
||||||
<div class="navbar-brand"><a class="navbar-item" href="overview.html"><img src="../../selfpost-wordmark.svg" alt="SelfPost"></a></div>
|
<div class="navbar-brand"><a class="navbar-item" href="overview.html"><img src="../../selfpost-wordmark.svg" alt="SelfPost"></a><details class="sp-touchnav"><summary>Menu</summary><div class="sp-touchnav-panel"><a href="overview.html">Overview</a><details class="sp-touchnav-group"><summary>Outbound</summary><a href="out-domains.html"><i class="ti ti-world-upload"></i>Domains</a><a href="out-log.html"><i class="ti ti-list-details"></i>Log</a><a href="out-queue.html"><i class="ti ti-stack-2"></i>Queue</a><a href="dmarc.html"><i class="ti ti-report-analytics"></i>DMARC reports</a></details><details class="sp-touchnav-group" open><summary>Inbound</summary><a href="in-domains.html" aria-current="page"><i class="ti ti-world-download"></i>Domains</a><a href="in-log.html"><i class="ti ti-list-details"></i>Log</a><a href="in-queue.html"><i class="ti ti-stack-2"></i>Queue</a><a href="in-quarantine.html"><i class="ti ti-mail-pause"></i>Quarantine</a><a href="in-filter-lists.html"><i class="ti ti-filter"></i>Filter lists</a></details><details class="sp-touchnav-group"><summary>Server</summary><a href="health.html"><i class="ti ti-heartbeat"></i>Health</a><a href="preflight.html"><i class="ti ti-checklist"></i>Preflight</a><a href="system-log.html"><i class="ti ti-file-text"></i>System log</a><a href="backup.html"><i class="ti ti-archive"></i>Backup</a><a href="users.html"><i class="ti ti-users"></i>Users</a><a href="settings.html"><i class="ti ti-settings"></i>Settings</a></details><details class="sp-touchnav-group"><summary><i class="ti ti-user-circle"></i>admin</summary><a href="account.html"><i class="ti ti-key"></i>Account</a><a href="help.html"><i class="ti ti-help"></i>Help</a><form method="post" action="login.html"><button type="submit"><i class="ti ti-logout"></i>Sign out</button></form></details></div></details></div>
|
||||||
<div class="navbar-menu is-active">
|
<div class="navbar-menu">
|
||||||
<div class="navbar-start">
|
<div class="navbar-start">
|
||||||
<a class="navbar-item" href="overview.html">Overview</a>
|
<a class="navbar-item" href="overview.html">Overview</a>
|
||||||
<div class="navbar-item has-dropdown is-hoverable">
|
<div class="navbar-item has-dropdown is-hoverable">
|
||||||
@@ -98,7 +98,7 @@
|
|||||||
<div class="sp-box-foot">The MX tag is green when at least one MX of the domain points at this server.</div>
|
<div class="sp-box-foot">The MX tag is green when at least one MX of the domain points at this server.</div>
|
||||||
</div>
|
</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 · Copyright © 2026 Mikhail Yenuchenko · <a href="#">License (AGPL-3.0)</a> · <a href="#">Source</a> · No warranty</footer>
|
||||||
</div>
|
</div>
|
||||||
</main>
|
</main>
|
||||||
</body>
|
</body>
|
||||||
|
|||||||
@@ -12,8 +12,8 @@
|
|||||||
<body>
|
<body>
|
||||||
<nav class="navbar is-primary" aria-label="main navigation">
|
<nav class="navbar is-primary" aria-label="main navigation">
|
||||||
<div class="container">
|
<div class="container">
|
||||||
<div class="navbar-brand"><a class="navbar-item" href="overview.html"><img src="../../selfpost-wordmark.svg" alt="SelfPost"></a></div>
|
<div class="navbar-brand"><a class="navbar-item" href="overview.html"><img src="../../selfpost-wordmark.svg" alt="SelfPost"></a><details class="sp-touchnav"><summary>Menu</summary><div class="sp-touchnav-panel"><a href="overview.html">Overview</a><details class="sp-touchnav-group"><summary>Outbound</summary><a href="out-domains.html"><i class="ti ti-world-upload"></i>Domains</a><a href="out-log.html"><i class="ti ti-list-details"></i>Log</a><a href="out-queue.html"><i class="ti ti-stack-2"></i>Queue</a><a href="dmarc.html"><i class="ti ti-report-analytics"></i>DMARC reports</a></details><details class="sp-touchnav-group" open><summary>Inbound</summary><a href="in-domains.html"><i class="ti ti-world-download"></i>Domains</a><a href="in-log.html"><i class="ti ti-list-details"></i>Log</a><a href="in-queue.html"><i class="ti ti-stack-2"></i>Queue</a><a href="in-quarantine.html"><i class="ti ti-mail-pause"></i>Quarantine</a><a href="in-filter-lists.html" aria-current="page"><i class="ti ti-filter"></i>Filter lists</a></details><details class="sp-touchnav-group"><summary>Server</summary><a href="health.html"><i class="ti ti-heartbeat"></i>Health</a><a href="preflight.html"><i class="ti ti-checklist"></i>Preflight</a><a href="system-log.html"><i class="ti ti-file-text"></i>System log</a><a href="backup.html"><i class="ti ti-archive"></i>Backup</a><a href="users.html"><i class="ti ti-users"></i>Users</a><a href="settings.html"><i class="ti ti-settings"></i>Settings</a></details><details class="sp-touchnav-group"><summary><i class="ti ti-user-circle"></i>admin</summary><a href="account.html"><i class="ti ti-key"></i>Account</a><a href="help.html"><i class="ti ti-help"></i>Help</a><form method="post" action="login.html"><button type="submit"><i class="ti ti-logout"></i>Sign out</button></form></details></div></details></div>
|
||||||
<div class="navbar-menu is-active">
|
<div class="navbar-menu">
|
||||||
<div class="navbar-start">
|
<div class="navbar-start">
|
||||||
<a class="navbar-item" href="overview.html">Overview</a>
|
<a class="navbar-item" href="overview.html">Overview</a>
|
||||||
<div class="navbar-item has-dropdown is-hoverable">
|
<div class="navbar-item has-dropdown is-hoverable">
|
||||||
@@ -132,13 +132,13 @@
|
|||||||
<dl class="sp-facts">
|
<dl class="sp-facts">
|
||||||
<div><dt>Applied by</dt><dd>rspamd, from map files it rereads within seconds of a change</dd></div>
|
<div><dt>Applied by</dt><dd>rspamd, from map files it rereads within seconds of a change</dd></div>
|
||||||
<div><dt>Files</dt><dd><span class="sp-mono">/data/antispam/</span> — six, one for each list and kind of entry</dd></div>
|
<div><dt>Files</dt><dd><span class="sp-mono">/data/antispam/</span> — six, one for each list and kind of entry</dd></div>
|
||||||
<div><dt>Last written</dt><dd>2026-09-11 08:40 UTC</dd></div>
|
<div><dt>Last written</dt><dd>2026-09-11 08:40</dd></div>
|
||||||
<div><dt>When both lists match</dt><dd>Deny wins, whichever entries they are.</dd></div>
|
<div><dt>When both lists match</dt><dd>Deny wins, whichever entries they are.</dd></div>
|
||||||
</dl>
|
</dl>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<footer class="sp-foot">SelfPost 1.8.0 · © 2026 Mix · <a href="#">License (AGPL-3.0)</a> · <a href="#">Source</a> · No warranty</footer>
|
<footer class="sp-foot">SelfPost 1.8.0 · Copyright © 2026 Mikhail Yenuchenko · <a href="#">License (AGPL-3.0)</a> · <a href="#">Source</a> · No warranty</footer>
|
||||||
</div>
|
</div>
|
||||||
</main>
|
</main>
|
||||||
</body>
|
</body>
|
||||||
|
|||||||
@@ -12,8 +12,8 @@
|
|||||||
<body>
|
<body>
|
||||||
<nav class="navbar is-primary" aria-label="main navigation">
|
<nav class="navbar is-primary" aria-label="main navigation">
|
||||||
<div class="container">
|
<div class="container">
|
||||||
<div class="navbar-brand"><a class="navbar-item" href="overview.html"><img src="../../selfpost-wordmark.svg" alt="SelfPost"></a></div>
|
<div class="navbar-brand"><a class="navbar-item" href="overview.html"><img src="../../selfpost-wordmark.svg" alt="SelfPost"></a><details class="sp-touchnav"><summary>Menu</summary><div class="sp-touchnav-panel"><a href="overview.html">Overview</a><details class="sp-touchnav-group"><summary>Outbound</summary><a href="out-domains.html"><i class="ti ti-world-upload"></i>Domains</a><a href="out-log.html"><i class="ti ti-list-details"></i>Log</a><a href="out-queue.html"><i class="ti ti-stack-2"></i>Queue</a><a href="dmarc.html"><i class="ti ti-report-analytics"></i>DMARC reports</a></details><details class="sp-touchnav-group" open><summary>Inbound</summary><a href="in-domains.html"><i class="ti ti-world-download"></i>Domains</a><a href="in-log.html" aria-current="page"><i class="ti ti-list-details"></i>Log</a><a href="in-queue.html"><i class="ti ti-stack-2"></i>Queue</a><a href="in-quarantine.html"><i class="ti ti-mail-pause"></i>Quarantine</a><a href="in-filter-lists.html"><i class="ti ti-filter"></i>Filter lists</a></details><details class="sp-touchnav-group"><summary>Server</summary><a href="health.html"><i class="ti ti-heartbeat"></i>Health</a><a href="preflight.html"><i class="ti ti-checklist"></i>Preflight</a><a href="system-log.html"><i class="ti ti-file-text"></i>System log</a><a href="backup.html"><i class="ti ti-archive"></i>Backup</a><a href="users.html"><i class="ti ti-users"></i>Users</a><a href="settings.html"><i class="ti ti-settings"></i>Settings</a></details><details class="sp-touchnav-group"><summary><i class="ti ti-user-circle"></i>admin</summary><a href="account.html"><i class="ti ti-key"></i>Account</a><a href="help.html"><i class="ti ti-help"></i>Help</a><form method="post" action="login.html"><button type="submit"><i class="ti ti-logout"></i>Sign out</button></form></details></div></details></div>
|
||||||
<div class="navbar-menu is-active">
|
<div class="navbar-menu">
|
||||||
<div class="navbar-start">
|
<div class="navbar-start">
|
||||||
<a class="navbar-item" href="overview.html">Overview</a>
|
<a class="navbar-item" href="overview.html">Overview</a>
|
||||||
<div class="navbar-item has-dropdown is-hoverable">
|
<div class="navbar-item has-dropdown is-hoverable">
|
||||||
@@ -69,7 +69,7 @@
|
|||||||
<div class="sp-grow">
|
<div class="sp-grow">
|
||||||
<p class="sp-kicker">Inbound</p>
|
<p class="sp-kicker">Inbound</p>
|
||||||
<h1 class="title is-3">Inbound log</h1>
|
<h1 class="title is-3">Inbound log</h1>
|
||||||
<p class="sp-lead">What port 25 decided about each message, one row per recipient. Accepted mail is recorded as it passes, refused mail is read from <code>mail.log</code>; the filter's verdict appears when the spam filter is on. Rows older than 90 days are deleted — change that in <a href="settings.html">Settings</a>.</p>
|
<p class="sp-lead">What port 25 decided about each message and what the upstream answered, one row per recipient. Accepted mail is recorded as it passes, refused mail is read from <code>mail.log</code>; the filter's verdict appears when the spam filter is on. Rows older than 90 days are deleted — change that in <a href="settings.html">Settings</a>.</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -86,20 +86,21 @@
|
|||||||
<a class="sp-help" href="help.html#inbound-log" title="What the decisions mean"><i class="ti ti-help-circle"></i></a>
|
<a class="sp-help" href="help.html#inbound-log" title="What the decisions 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>Decision</th><th></th></tr></thead>
|
<thead><tr><th>Time</th><th>From</th><th>To</th><th>Subject</th><th>Decision</th><th>Upstream</th><th></th></tr></thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
<tr><td class="sp-mono sp-muted sp-nowrap sp-small">09-21 14:00:18</td><td>t.berg@<wbr>uni.example<br><span class="sp-mono sp-muted sp-small">192.0.2.77</span></td><td>dev@<wbr>lists.example.org</td><td class="sp-clip">Re: [dev] patch series v3</td><td><button type="button" class="tag is-success is-light" popovertarget="why-1">accept</button><div class="sp-sheet" id="why-1" popover><div class="sp-sheet-in"><div class="sp-sheet-head"><span class="tag is-success is-light">accept</span><h2>Why</h2><button type="button" popovertarget="why-1" popovertargetaction="hide" aria-label="Close"><i class="ti ti-x"></i></button></div><div class="sp-sheet-body"><div class="table-container"><table class="table is-fullwidth"><tbody><tr><th>Decided by</th><td>rspamd</td></tr><tr><th>Verdict</th><td>no action, score 0.40 / 15.00</td></tr><tr><th>Subject</th><td>Re: [dev] patch series v3</td></tr></tbody></table></div></div></div></div></td><td class="sp-actions"><a href="in-filter-lists.html">Deny sender</a></td></tr>
|
<tr><td class="sp-mono sp-muted sp-nowrap sp-small">09-21 14:00:31</td><td>ci@<wbr>builds.example<br><span class="sp-mono sp-muted sp-small">192.0.2.90</span></td><td>dev@<wbr>lists.example.org</td><td class="sp-clip">[ci] nightly build passed</td><td><button type="button" class="tag is-success is-light" popovertarget="why-7">accept</button><div class="sp-sheet" id="why-7" popover><div class="sp-sheet-in"><div class="sp-sheet-head"><span class="tag is-success is-light">accept</span><h2>Why</h2><button type="button" popovertarget="why-7" popovertargetaction="hide" aria-label="Close"><i class="ti ti-x"></i></button></div><div class="sp-sheet-body"><div class="table-container"><table class="table is-fullwidth"><tbody><tr><th>Decided by</th><td>rspamd</td></tr><tr><th>Verdict</th><td>no action, score 1.10 / 15.00</td></tr><tr><th>Subject</th><td>[ci] nightly build passed</td></tr></tbody></table></div></div></div></div></td><td><span class="tag is-light">queued</span></td><td class="sp-actions"><a href="in-filter-lists.html">Deny sender</a></td></tr>
|
||||||
<tr><td class="sp-mono sp-muted sp-nowrap sp-small">09-21 13:59:02</td><td>invoice@<wbr>partner-billing.example<br><span class="sp-mono sp-muted sp-small">203.0.113.140</span></td><td>owner@<wbr>lists.example.org</td><td class="sp-clip">Outstanding invoice — action required</td><td><button type="button" class="tag is-success is-light" popovertarget="why-2">accept</button><div class="sp-sheet" id="why-2" popover><div class="sp-sheet-in"><div class="sp-sheet-head"><span class="tag is-success is-light">accept</span><h2>Why</h2><button type="button" popovertarget="why-2" popovertargetaction="hide" aria-label="Close"><i class="ti ti-x"></i></button></div><div class="sp-sheet-body"><div class="table-container"><table class="table is-fullwidth"><tbody><tr><th>Decided by</th><td>rspamd</td></tr><tr><th>Verdict</th><td>add header, score 9.80 / 15.00</td></tr><tr><th>Symbols</th><td class="sp-mono sp-small">PHISHING(5.00)<br>FORGED_SENDER(3.00)<br>R_SPF_FAIL(1.00)</td></tr><tr><th>Subject</th><td>Outstanding invoice — action required</td></tr></tbody></table></div></div></div></div></td><td class="sp-actions"><a href="in-filter-lists.html">Deny sender</a></td></tr>
|
<tr><td class="sp-mono sp-muted sp-nowrap sp-small">09-21 14:00:18</td><td>t.berg@<wbr>uni.example<br><span class="sp-mono sp-muted sp-small">192.0.2.77</span></td><td>dev@<wbr>lists.example.org</td><td class="sp-clip">Re: [dev] patch series v3</td><td><button type="button" class="tag is-success is-light" popovertarget="why-1">accept</button><div class="sp-sheet" id="why-1" popover><div class="sp-sheet-in"><div class="sp-sheet-head"><span class="tag is-success is-light">accept</span><h2>Why</h2><button type="button" popovertarget="why-1" popovertargetaction="hide" aria-label="Close"><i class="ti ti-x"></i></button></div><div class="sp-sheet-body"><div class="table-container"><table class="table is-fullwidth"><tbody><tr><th>Decided by</th><td>rspamd</td></tr><tr><th>Verdict</th><td>no action, score 0.40 / 15.00</td></tr><tr><th>Subject</th><td>Re: [dev] patch series v3</td></tr></tbody></table></div></div></div></div></td><td><button type="button" class="tag is-success is-light" popovertarget="up-1">sent</button><div class="sp-sheet" id="up-1" popover><div class="sp-sheet-in"><div class="sp-sheet-head"><span class="tag is-success is-light">sent</span><h2>Upstream</h2><button type="button" popovertarget="up-1" popovertargetaction="hide" aria-label="Close"><i class="ti ti-x"></i></button></div><div class="sp-sheet-body"><div class="table-container"><table class="table is-fullwidth"><tbody><tr><th>Host</th><td class="sp-mono sp-small">mx.upstream.example[198.51.100.5]:25</td></tr><tr><th>Reply</th><td class="sp-mono sp-small">250 2.0.0 Ok: queued as CE05D10261D</td></tr><tr><th>At</th><td class="sp-mono sp-small">09-21 14:00:19</td></tr></tbody></table></div></div></div></div></td><td class="sp-actions"><a href="in-filter-lists.html">Deny sender</a></td></tr>
|
||||||
<tr><td class="sp-mono sp-muted sp-nowrap sp-small">09-21 13:57:41</td><td>promo@<wbr>bulk.example<br><span class="sp-mono sp-muted sp-small">198.51.100.90</span></td><td>sales@<wbr>lists.example.org</td><td class="sp-clip sp-muted">—</td><td><button type="button" class="tag is-danger is-light" popovertarget="why-3">reject</button><div class="sp-sheet" id="why-3" popover><div class="sp-sheet-in"><div class="sp-sheet-head"><span class="tag is-danger is-light">reject</span><h2>Why</h2><button type="button" popovertarget="why-3" popovertargetaction="hide" aria-label="Close"><i class="ti ti-x"></i></button></div><div class="sp-sheet-body"><div class="table-container"><table class="table is-fullwidth"><tbody><tr><th>Decided by</th><td>postfix</td></tr><tr><th>Reply</th><td class="sp-mono sp-small">550 5.1.1 <sales@lists.example.org>: Recipient address rejected: User unknown in relay recipient table</td></tr></tbody></table></div></div></div></div></td><td class="sp-actions"></td></tr>
|
<tr><td class="sp-mono sp-muted sp-nowrap sp-small">09-21 13:59:02</td><td>invoice@<wbr>partner-billing.example<br><span class="sp-mono sp-muted sp-small">203.0.113.140</span></td><td>owner@<wbr>lists.example.org</td><td class="sp-clip">Outstanding invoice — action required</td><td><button type="button" class="tag is-success is-light" popovertarget="why-2">accept</button><div class="sp-sheet" id="why-2" popover><div class="sp-sheet-in"><div class="sp-sheet-head"><span class="tag is-success is-light">accept</span><h2>Why</h2><button type="button" popovertarget="why-2" popovertargetaction="hide" aria-label="Close"><i class="ti ti-x"></i></button></div><div class="sp-sheet-body"><div class="table-container"><table class="table is-fullwidth"><tbody><tr><th>Decided by</th><td>rspamd</td></tr><tr><th>Verdict</th><td>add header, score 9.80 / 15.00</td></tr><tr><th>Symbols</th><td class="sp-mono sp-small">PHISHING(5.00)<br>FORGED_SENDER(3.00)<br>R_SPF_FAIL(1.00)</td></tr><tr><th>Subject</th><td>Outstanding invoice — action required</td></tr></tbody></table></div></div></div></div></td><td><button type="button" class="tag is-danger is-light" popovertarget="up-2">bounced</button><div class="sp-sheet" id="up-2" popover><div class="sp-sheet-in"><div class="sp-sheet-head"><span class="tag is-danger is-light">bounced</span><h2>Upstream</h2><button type="button" popovertarget="up-2" popovertargetaction="hide" aria-label="Close"><i class="ti ti-x"></i></button></div><div class="sp-sheet-body"><div class="table-container"><table class="table is-fullwidth"><tbody><tr><th>Host</th><td class="sp-mono sp-small">mx.upstream.example[198.51.100.5]:25</td></tr><tr><th>Reply</th><td class="sp-mono sp-small">550 5.7.1 Message rejected as spam by the upstream filter</td></tr><tr><th>At</th><td class="sp-mono sp-small">09-21 13:59:04</td></tr><tr><th>Then</th><td>The sender was sent a bounce</td></tr></tbody></table></div></div></div></div></td><td class="sp-actions"><a href="in-filter-lists.html">Deny sender</a></td></tr>
|
||||||
<tr><td class="sp-mono sp-muted sp-nowrap sp-small">09-21 13:51:30</td><td>billing@<wbr>partner.example<br><span class="sp-mono sp-muted sp-small">198.51.100.12</span></td><td>owner@<wbr>lists.example.org</td><td class="sp-clip sp-muted">—</td><td><button type="button" class="tag is-danger is-light" popovertarget="why-4">reject</button><div class="sp-sheet" id="why-4" popover><div class="sp-sheet-in"><div class="sp-sheet-head"><span class="tag is-danger is-light">reject</span><h2>Why</h2><button type="button" popovertarget="why-4" popovertargetaction="hide" aria-label="Close"><i class="ti ti-x"></i></button></div><div class="sp-sheet-body"><div class="table-container"><table class="table is-fullwidth"><tbody><tr><th>Decided by</th><td>rspamd</td></tr><tr><th>Reply</th><td class="sp-mono sp-small">5.7.1 Spam message rejected</td></tr></tbody></table></div></div></div></div></td><td class="sp-actions"><a href="in-filter-lists.html">Allow sender</a></td></tr>
|
<tr><td class="sp-mono sp-muted sp-nowrap sp-small">09-21 13:57:41</td><td>promo@<wbr>bulk.example<br><span class="sp-mono sp-muted sp-small">198.51.100.90</span></td><td>sales@<wbr>lists.example.org</td><td class="sp-clip sp-muted">—</td><td><button type="button" class="tag is-danger is-light" popovertarget="why-3">reject</button><div class="sp-sheet" id="why-3" popover><div class="sp-sheet-in"><div class="sp-sheet-head"><span class="tag is-danger is-light">reject</span><h2>Why</h2><button type="button" popovertarget="why-3" popovertargetaction="hide" aria-label="Close"><i class="ti ti-x"></i></button></div><div class="sp-sheet-body"><div class="table-container"><table class="table is-fullwidth"><tbody><tr><th>Decided by</th><td>postfix</td></tr><tr><th>Reply</th><td class="sp-mono sp-small">550 5.1.1 <sales@lists.example.org>: Recipient address rejected: User unknown in relay recipient table</td></tr></tbody></table></div></div></div></div></td><td class="sp-muted">—</td><td class="sp-actions"></td></tr>
|
||||||
<tr><td class="sp-mono sp-muted sp-nowrap sp-small">09-21 13:44:02</td><td>hr@<wbr>partner.example<br><span class="sp-mono sp-muted sp-small">192.0.2.31</span></td><td>jobs@<wbr>acme.io</td><td class="sp-clip sp-muted">—</td><td><button type="button" class="tag is-warning is-light" popovertarget="why-5">tempfail</button><div class="sp-sheet" id="why-5" popover><div class="sp-sheet-in"><div class="sp-sheet-head"><span class="tag is-warning is-light">tempfail</span><h2>Why</h2><button type="button" popovertarget="why-5" popovertargetaction="hide" aria-label="Close"><i class="ti ti-x"></i></button></div><div class="sp-sheet-body"><div class="table-container"><table class="table is-fullwidth"><tbody><tr><th>Decided by</th><td>rspamd</td></tr><tr><th>Reply</th><td class="sp-mono sp-small">4.7.1 Try again later</td></tr></tbody></table></div></div></div></div></td><td class="sp-actions"><a href="in-filter-lists.html">Allow sender</a></td></tr>
|
<tr><td class="sp-mono sp-muted sp-nowrap sp-small">09-21 13:51:30</td><td>billing@<wbr>partner.example<br><span class="sp-mono sp-muted sp-small">198.51.100.12</span></td><td>owner@<wbr>lists.example.org</td><td class="sp-clip sp-muted">—</td><td><button type="button" class="tag is-danger is-light" popovertarget="why-4">reject</button><div class="sp-sheet" id="why-4" popover><div class="sp-sheet-in"><div class="sp-sheet-head"><span class="tag is-danger is-light">reject</span><h2>Why</h2><button type="button" popovertarget="why-4" popovertargetaction="hide" aria-label="Close"><i class="ti ti-x"></i></button></div><div class="sp-sheet-body"><div class="table-container"><table class="table is-fullwidth"><tbody><tr><th>Decided by</th><td>rspamd</td></tr><tr><th>Reply</th><td class="sp-mono sp-small">5.7.1 Spam message rejected</td></tr></tbody></table></div></div></div></div></td><td class="sp-muted">—</td><td class="sp-actions"><a href="in-filter-lists.html">Allow sender</a></td></tr>
|
||||||
<tr><td class="sp-mono sp-muted sp-nowrap sp-small">09-21 13:30:55</td><td>noreply@<wbr>github.example<br><span class="sp-mono sp-muted sp-small">192.0.2.200</span></td><td>security@<wbr>lists.example.org</td><td class="sp-clip">[advisory] new report</td><td><button type="button" class="tag is-success is-light" popovertarget="why-6">accept</button><div class="sp-sheet" id="why-6" popover><div class="sp-sheet-in"><div class="sp-sheet-head"><span class="tag is-success is-light">accept</span><h2>Why</h2><button type="button" popovertarget="why-6" popovertargetaction="hide" aria-label="Close"><i class="ti ti-x"></i></button></div><div class="sp-sheet-body"><div class="table-container"><table class="table is-fullwidth"><tbody><tr><th>Decided by</th><td>rspamd</td></tr><tr><th>Verdict</th><td>no action, score -98.10 / 15.00</td></tr><tr><th>Symbols</th><td class="sp-mono sp-small">SELFPOST_ALLOW_FROM(-100.00)<br>R_DKIM_ALLOW(-0.20)</td></tr><tr><th>Subject</th><td>[advisory] new report</td></tr></tbody></table></div></div></div></div></td><td class="sp-actions"><a href="in-filter-lists.html">Deny sender</a></td></tr>
|
<tr><td class="sp-mono sp-muted sp-nowrap sp-small">09-21 13:44:02</td><td>hr@<wbr>partner.example<br><span class="sp-mono sp-muted sp-small">192.0.2.31</span></td><td>jobs@<wbr>acme.io</td><td class="sp-clip sp-muted">—</td><td><button type="button" class="tag is-warning is-light" popovertarget="why-5">tempfail</button><div class="sp-sheet" id="why-5" popover><div class="sp-sheet-in"><div class="sp-sheet-head"><span class="tag is-warning is-light">tempfail</span><h2>Why</h2><button type="button" popovertarget="why-5" popovertargetaction="hide" aria-label="Close"><i class="ti ti-x"></i></button></div><div class="sp-sheet-body"><div class="table-container"><table class="table is-fullwidth"><tbody><tr><th>Decided by</th><td>rspamd</td></tr><tr><th>Reply</th><td class="sp-mono sp-small">4.7.1 Try again later</td></tr></tbody></table></div></div></div></div></td><td class="sp-muted">—</td><td class="sp-actions"><a href="in-filter-lists.html">Allow sender</a></td></tr>
|
||||||
|
<tr><td class="sp-mono sp-muted sp-nowrap sp-small">09-21 13:30:55</td><td>noreply@<wbr>github.example<br><span class="sp-mono sp-muted sp-small">192.0.2.200</span></td><td>security@<wbr>lists.example.org</td><td class="sp-clip">[advisory] new report</td><td><button type="button" class="tag is-success is-light" popovertarget="why-6">accept</button><div class="sp-sheet" id="why-6" popover><div class="sp-sheet-in"><div class="sp-sheet-head"><span class="tag is-success is-light">accept</span><h2>Why</h2><button type="button" popovertarget="why-6" popovertargetaction="hide" aria-label="Close"><i class="ti ti-x"></i></button></div><div class="sp-sheet-body"><div class="table-container"><table class="table is-fullwidth"><tbody><tr><th>Decided by</th><td>rspamd</td></tr><tr><th>Verdict</th><td>no action, score -98.10 / 15.00</td></tr><tr><th>Symbols</th><td class="sp-mono sp-small">SELFPOST_ALLOW_FROM(-100.00)<br>R_DKIM_ALLOW(-0.20)</td></tr><tr><th>Subject</th><td>[advisory] new report</td></tr></tbody></table></div></div></div></div></td><td><button type="button" class="tag is-warning is-light" popovertarget="up-6">deferred</button><div class="sp-sheet" id="up-6" popover><div class="sp-sheet-in"><div class="sp-sheet-head"><span class="tag is-warning is-light">deferred</span><h2>Upstream</h2><button type="button" popovertarget="up-6" popovertargetaction="hide" aria-label="Close"><i class="ti ti-x"></i></button></div><div class="sp-sheet-body"><div class="table-container"><table class="table is-fullwidth"><tbody><tr><th>Reply</th><td class="sp-mono sp-small">connect to mx.upstream.example[198.51.100.5]:25: Connection refused</td></tr><tr><th>At</th><td class="sp-mono sp-small">09-21 13:45:12</td></tr><tr><th>Then</th><td>Postfix tries again; the message is in the queue</td></tr></tbody></table></div></div></div></div></td><td class="sp-actions"><a href="in-filter-lists.html">Deny sender</a></td></tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
</table></div>
|
</table></div>
|
||||||
<div class="sp-box-foot"><span>Page 1 of 38</span><a href="#">Older →</a><span class="sp-end">A decision opens its reason · Allow / Deny sender opens <a href="in-filter-lists.html">Filter lists</a> with the entry filled in</span></div>
|
<div class="sp-box-foot"><span>Page 1 of 38</span><a href="#">Older →</a><span class="sp-end">A decision opens its reason, an upstream result its last reply · Allow / Deny sender opens <a href="in-filter-lists.html">Filter lists</a> with the entry filled in</span></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 · Copyright © 2026 Mikhail Yenuchenko · <a href="#">License (AGPL-3.0)</a> · <a href="#">Source</a> · No warranty</footer>
|
||||||
</div>
|
</div>
|
||||||
</main>
|
</main>
|
||||||
</body>
|
</body>
|
||||||
|
|||||||
@@ -12,8 +12,8 @@
|
|||||||
<body>
|
<body>
|
||||||
<nav class="navbar is-primary" aria-label="main navigation">
|
<nav class="navbar is-primary" aria-label="main navigation">
|
||||||
<div class="container">
|
<div class="container">
|
||||||
<div class="navbar-brand"><a class="navbar-item" href="overview.html"><img src="../../selfpost-wordmark.svg" alt="SelfPost"></a></div>
|
<div class="navbar-brand"><a class="navbar-item" href="overview.html"><img src="../../selfpost-wordmark.svg" alt="SelfPost"></a><details class="sp-touchnav"><summary>Menu</summary><div class="sp-touchnav-panel"><a href="overview.html">Overview</a><details class="sp-touchnav-group"><summary>Outbound</summary><a href="out-domains.html"><i class="ti ti-world-upload"></i>Domains</a><a href="out-log.html"><i class="ti ti-list-details"></i>Log</a><a href="out-queue.html"><i class="ti ti-stack-2"></i>Queue</a><a href="dmarc.html"><i class="ti ti-report-analytics"></i>DMARC reports</a></details><details class="sp-touchnav-group" open><summary>Inbound</summary><a href="in-domains.html"><i class="ti ti-world-download"></i>Domains</a><a href="in-log.html"><i class="ti ti-list-details"></i>Log</a><a href="in-queue.html"><i class="ti ti-stack-2"></i>Queue</a><a href="in-quarantine.html" aria-current="page"><i class="ti ti-mail-pause"></i>Quarantine</a><a href="in-filter-lists.html"><i class="ti ti-filter"></i>Filter lists</a></details><details class="sp-touchnav-group"><summary>Server</summary><a href="health.html"><i class="ti ti-heartbeat"></i>Health</a><a href="preflight.html"><i class="ti ti-checklist"></i>Preflight</a><a href="system-log.html"><i class="ti ti-file-text"></i>System log</a><a href="backup.html"><i class="ti ti-archive"></i>Backup</a><a href="users.html"><i class="ti ti-users"></i>Users</a><a href="settings.html"><i class="ti ti-settings"></i>Settings</a></details><details class="sp-touchnav-group"><summary><i class="ti ti-user-circle"></i>admin</summary><a href="account.html"><i class="ti ti-key"></i>Account</a><a href="help.html"><i class="ti ti-help"></i>Help</a><form method="post" action="login.html"><button type="submit"><i class="ti ti-logout"></i>Sign out</button></form></details></div></details></div>
|
||||||
<div class="navbar-menu is-active">
|
<div class="navbar-menu">
|
||||||
<div class="navbar-start">
|
<div class="navbar-start">
|
||||||
<a class="navbar-item" href="overview.html">Overview</a>
|
<a class="navbar-item" href="overview.html">Overview</a>
|
||||||
<div class="navbar-item has-dropdown is-hoverable">
|
<div class="navbar-item has-dropdown is-hoverable">
|
||||||
@@ -81,7 +81,7 @@
|
|||||||
<dl class="sp-facts">
|
<dl class="sp-facts">
|
||||||
<div><dt>Inbound domain</dt><dd><a href="in-domain.html">lists.example.org</a></dd></div>
|
<div><dt>Inbound domain</dt><dd><a href="in-domain.html">lists.example.org</a></dd></div>
|
||||||
<div><dt>Client</dt><dd class="sp-mono">203.0.113.140</dd></div>
|
<div><dt>Client</dt><dd class="sp-mono">203.0.113.140</dd></div>
|
||||||
<div><dt>Held</dt><dd>2026-09-21 13:59:02 UTC</dd></div>
|
<div><dt>Held</dt><dd>2026-09-21 13:59:02</dd></div>
|
||||||
<div><dt>Deleted on</dt><dd>2026-10-05 · in 14 days</dd></div>
|
<div><dt>Deleted on</dt><dd>2026-10-05 · in 14 days</dd></div>
|
||||||
<div><dt>Size</dt><dd>48 kB · 1 attachment (<span class="sp-mono">invoice.html</span>)</dd></div>
|
<div><dt>Size</dt><dd>48 kB · 1 attachment (<span class="sp-mono">invoice.html</span>)</dd></div>
|
||||||
<div><dt>Would go to</dt><dd class="sp-mono">mx.internal:25</dd></div>
|
<div><dt>Would go to</dt><dd class="sp-mono">mx.internal:25</dd></div>
|
||||||
@@ -106,13 +106,14 @@
|
|||||||
|
|
||||||
<div class="box">
|
<div class="box">
|
||||||
<div class="sp-box-head"><span class="sp-no">03</span><h2>Headers</h2><div class="sp-end">The body and attachments are never rendered in the panel</div></div>
|
<div class="sp-box-head"><span class="sp-no">03</span><h2>Headers</h2><div class="sp-end">The body and attachments are never rendered in the panel</div></div>
|
||||||
<pre class="sp-log"><span class="sp-d">Received:</span> from mail.partner-billing.example (unknown [203.0.113.140]) by mail.example.org
|
<pre class="sp-log"><span class="sp-ll"><span class="sp-d">Received:</span> from mail.partner-billing.example (unknown [203.0.113.140]) by mail.example.org
|
||||||
<span class="sp-d">From:</span> "Partner Billing" <invoice@partner-billing.example>
|
</span><span class="sp-ll"><span class="sp-d">From:</span> "Partner Billing" <invoice@partner-billing.example>
|
||||||
<span class="sp-d">Reply-To:</span> <span class="sp-w">payments.dept@freemail.example</span>
|
</span><span class="sp-ll"><span class="sp-d">Reply-To:</span> <span class="sp-w">payments.dept@freemail.example</span>
|
||||||
<span class="sp-d">To:</span> owner@lists.example.org
|
</span><span class="sp-ll"><span class="sp-d">To:</span> owner@lists.example.org
|
||||||
<span class="sp-d">Subject:</span> Outstanding invoice — action required
|
</span><span class="sp-ll"><span class="sp-d">Subject:</span> Outstanding invoice — action required
|
||||||
<span class="sp-d">Date:</span> Mon, 21 Sep 2026 13:58:57 +0000
|
</span><span class="sp-ll"><span class="sp-d">Date:</span> Mon, 21 Sep 2026 13:58:57 +0000
|
||||||
<span class="sp-d">Authentication-Results:</span> <span class="sp-e">spf=fail dkim=none dmarc=fail</span></pre>
|
</span><span class="sp-ll"><span class="sp-d">Authentication-Results:</span> <span class="sp-e">spf=fail dkim=none dmarc=fail</span>
|
||||||
|
</span></pre>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="columns">
|
<div class="columns">
|
||||||
@@ -142,7 +143,7 @@
|
|||||||
</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 · Copyright © 2026 Mikhail Yenuchenko · <a href="#">License (AGPL-3.0)</a> · <a href="#">Source</a> · No warranty</footer>
|
||||||
</div>
|
</div>
|
||||||
</main>
|
</main>
|
||||||
</body>
|
</body>
|
||||||
|
|||||||
@@ -12,8 +12,8 @@
|
|||||||
<body>
|
<body>
|
||||||
<nav class="navbar is-primary" aria-label="main navigation">
|
<nav class="navbar is-primary" aria-label="main navigation">
|
||||||
<div class="container">
|
<div class="container">
|
||||||
<div class="navbar-brand"><a class="navbar-item" href="overview.html"><img src="../../selfpost-wordmark.svg" alt="SelfPost"></a></div>
|
<div class="navbar-brand"><a class="navbar-item" href="overview.html"><img src="../../selfpost-wordmark.svg" alt="SelfPost"></a><details class="sp-touchnav"><summary>Menu</summary><div class="sp-touchnav-panel"><a href="overview.html">Overview</a><details class="sp-touchnav-group"><summary>Outbound</summary><a href="out-domains.html"><i class="ti ti-world-upload"></i>Domains</a><a href="out-log.html"><i class="ti ti-list-details"></i>Log</a><a href="out-queue.html"><i class="ti ti-stack-2"></i>Queue</a><a href="dmarc.html"><i class="ti ti-report-analytics"></i>DMARC reports</a></details><details class="sp-touchnav-group" open><summary>Inbound</summary><a href="in-domains.html"><i class="ti ti-world-download"></i>Domains</a><a href="in-log.html"><i class="ti ti-list-details"></i>Log</a><a href="in-queue.html"><i class="ti ti-stack-2"></i>Queue</a><a href="in-quarantine.html" aria-current="page"><i class="ti ti-mail-pause"></i>Quarantine</a><a href="in-filter-lists.html"><i class="ti ti-filter"></i>Filter lists</a></details><details class="sp-touchnav-group"><summary>Server</summary><a href="health.html"><i class="ti ti-heartbeat"></i>Health</a><a href="preflight.html"><i class="ti ti-checklist"></i>Preflight</a><a href="system-log.html"><i class="ti ti-file-text"></i>System log</a><a href="backup.html"><i class="ti ti-archive"></i>Backup</a><a href="users.html"><i class="ti ti-users"></i>Users</a><a href="settings.html"><i class="ti ti-settings"></i>Settings</a></details><details class="sp-touchnav-group"><summary><i class="ti ti-user-circle"></i>admin</summary><a href="account.html"><i class="ti ti-key"></i>Account</a><a href="help.html"><i class="ti ti-help"></i>Help</a><form method="post" action="login.html"><button type="submit"><i class="ti ti-logout"></i>Sign out</button></form></details></div></details></div>
|
||||||
<div class="navbar-menu is-active">
|
<div class="navbar-menu">
|
||||||
<div class="navbar-start">
|
<div class="navbar-start">
|
||||||
<a class="navbar-item" href="overview.html">Overview</a>
|
<a class="navbar-item" href="overview.html">Overview</a>
|
||||||
<div class="navbar-item has-dropdown is-hoverable">
|
<div class="navbar-item has-dropdown is-hoverable">
|
||||||
@@ -82,7 +82,7 @@
|
|||||||
</form>
|
</form>
|
||||||
</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>Held, UTC</th><th>From</th><th>To</th><th>Subject</th><th>Why</th><th>Size</th><th>Deleted in</th><th></th></tr></thead>
|
<thead><tr><th>Held</th><th>From</th><th>To</th><th>Subject</th><th>Why</th><th>Size</th><th>Deleted in</th><th></th></tr></thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
<tr><td class="sp-mono sp-muted sp-nowrap sp-small">09-21 13:59:02</td><td>invoice@<wbr>partner-billing.example</td><td>owner@<wbr>lists.example.org</td><td class="sp-clip"><a href="in-quarantine-message.html"><strong>Outstanding invoice — action required</strong></a></td><td class="sp-muted sp-small">score 9.8 · PHISHING</td><td class="sp-muted sp-nowrap">48 kB</td><td class="sp-nowrap">14 days</td><td class="sp-actions"><a href="in-quarantine-message.html">Review</a></td></tr>
|
<tr><td class="sp-mono sp-muted sp-nowrap sp-small">09-21 13:59:02</td><td>invoice@<wbr>partner-billing.example</td><td>owner@<wbr>lists.example.org</td><td class="sp-clip"><a href="in-quarantine-message.html"><strong>Outstanding invoice — action required</strong></a></td><td class="sp-muted sp-small">score 9.8 · PHISHING</td><td class="sp-muted sp-nowrap">48 kB</td><td class="sp-nowrap">14 days</td><td class="sp-actions"><a href="in-quarantine-message.html">Review</a></td></tr>
|
||||||
<tr><td class="sp-mono sp-muted sp-nowrap sp-small">09-20 22:14:40</td><td>no-reply@<wbr>shipping-update.example</td><td>jobs@<wbr>acme.io</td><td class="sp-clip"><a href="in-quarantine-message.html"><strong>Your parcel could not be delivered</strong></a></td><td class="sp-muted sp-small">score 8.1 · URL_SHORTENER</td><td class="sp-muted sp-nowrap">12 kB</td><td class="sp-nowrap">13 days</td><td class="sp-actions"><a href="in-quarantine-message.html">Review</a></td></tr>
|
<tr><td class="sp-mono sp-muted sp-nowrap sp-small">09-20 22:14:40</td><td>no-reply@<wbr>shipping-update.example</td><td>jobs@<wbr>acme.io</td><td class="sp-clip"><a href="in-quarantine-message.html"><strong>Your parcel could not be delivered</strong></a></td><td class="sp-muted sp-small">score 8.1 · URL_SHORTENER</td><td class="sp-muted sp-nowrap">12 kB</td><td class="sp-nowrap">13 days</td><td class="sp-actions"><a href="in-quarantine-message.html">Review</a></td></tr>
|
||||||
@@ -92,7 +92,7 @@
|
|||||||
<div class="sp-box-foot"><span>3 messages · 3.0 MB of 500 MB</span><span class="sp-end">A domain administrator sees only their assigned inbound domains</span></div>
|
<div class="sp-box-foot"><span>3 messages · 3.0 MB of 500 MB</span><span class="sp-end">A domain administrator sees only their assigned inbound domains</span></div>
|
||||||
</div>
|
</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 · Copyright © 2026 Mikhail Yenuchenko · <a href="#">License (AGPL-3.0)</a> · <a href="#">Source</a> · No warranty</footer>
|
||||||
</div>
|
</div>
|
||||||
</main>
|
</main>
|
||||||
</body>
|
</body>
|
||||||
|
|||||||
@@ -12,8 +12,8 @@
|
|||||||
<body>
|
<body>
|
||||||
<nav class="navbar is-primary" aria-label="main navigation">
|
<nav class="navbar is-primary" aria-label="main navigation">
|
||||||
<div class="container">
|
<div class="container">
|
||||||
<div class="navbar-brand"><a class="navbar-item" href="overview.html"><img src="../../selfpost-wordmark.svg" alt="SelfPost"></a></div>
|
<div class="navbar-brand"><a class="navbar-item" href="overview.html"><img src="../../selfpost-wordmark.svg" alt="SelfPost"></a><details class="sp-touchnav"><summary>Menu</summary><div class="sp-touchnav-panel"><a href="overview.html">Overview</a><details class="sp-touchnav-group"><summary>Outbound</summary><a href="out-domains.html"><i class="ti ti-world-upload"></i>Domains</a><a href="out-log.html"><i class="ti ti-list-details"></i>Log</a><a href="out-queue.html"><i class="ti ti-stack-2"></i>Queue</a><a href="dmarc.html"><i class="ti ti-report-analytics"></i>DMARC reports</a></details><details class="sp-touchnav-group" open><summary>Inbound</summary><a href="in-domains.html"><i class="ti ti-world-download"></i>Domains</a><a href="in-log.html"><i class="ti ti-list-details"></i>Log</a><a href="in-queue.html" aria-current="page"><i class="ti ti-stack-2"></i>Queue</a><a href="in-quarantine.html"><i class="ti ti-mail-pause"></i>Quarantine</a><a href="in-filter-lists.html"><i class="ti ti-filter"></i>Filter lists</a></details><details class="sp-touchnav-group"><summary>Server</summary><a href="health.html"><i class="ti ti-heartbeat"></i>Health</a><a href="preflight.html"><i class="ti ti-checklist"></i>Preflight</a><a href="system-log.html"><i class="ti ti-file-text"></i>System log</a><a href="backup.html"><i class="ti ti-archive"></i>Backup</a><a href="users.html"><i class="ti ti-users"></i>Users</a><a href="settings.html"><i class="ti ti-settings"></i>Settings</a></details><details class="sp-touchnav-group"><summary><i class="ti ti-user-circle"></i>admin</summary><a href="account.html"><i class="ti ti-key"></i>Account</a><a href="help.html"><i class="ti ti-help"></i>Help</a><form method="post" action="login.html"><button type="submit"><i class="ti ti-logout"></i>Sign out</button></form></details></div></details></div>
|
||||||
<div class="navbar-menu is-active">
|
<div class="navbar-menu">
|
||||||
<div class="navbar-start">
|
<div class="navbar-start">
|
||||||
<a class="navbar-item" href="overview.html">Overview</a>
|
<a class="navbar-item" href="overview.html">Overview</a>
|
||||||
<div class="navbar-item has-dropdown is-hoverable">
|
<div class="navbar-item has-dropdown is-hoverable">
|
||||||
@@ -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">Refreshes every 10 s</div></div>
|
<div class="sp-box-head"><span class="sp-no">01</span><h2>Waiting</h2><div class="sp-end">Refreshes on its own</div></div>
|
||||||
<div class="sp-empty"><i class="ti ti-stack-2"></i>Nothing is waiting. Every accepted message has reached its upstream.</div>
|
<div class="sp-empty"><i class="ti ti-stack-2"></i>Nothing is waiting. Every accepted message has reached its upstream.</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -90,7 +90,7 @@
|
|||||||
</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 · Copyright © 2026 Mikhail Yenuchenko · <a href="#">License (AGPL-3.0)</a> · <a href="#">Source</a> · No warranty</footer>
|
||||||
</div>
|
</div>
|
||||||
</main>
|
</main>
|
||||||
</body>
|
</body>
|
||||||
|
|||||||
@@ -23,7 +23,7 @@
|
|||||||
<h1 class="title is-3">No administrator yet</h1>
|
<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>
|
<p class="sp-muted">Open the one-time setup link printed in the container log to create one.</p>
|
||||||
</div>
|
</div>
|
||||||
<p class="sp-foot">© 2026 Mix · <a href="#">License (AGPL-3.0)</a> · <a href="#">Source</a> · No warranty</p>
|
<p class="sp-foot">Copyright © 2026 Mikhail Yenuchenko · <a href="#">License (AGPL-3.0)</a> · <a href="#">Source</a> · No warranty</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,41 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en" data-theme="light">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
|
<title>Sign in · SelfPost</title>
|
||||||
|
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bulma@1.0.4/css/bulma.min.css">
|
||||||
|
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@tabler/icons-webfont@3/dist/tabler-icons.min.css">
|
||||||
|
<link rel="stylesheet" href="../shared/brand.css">
|
||||||
|
<link rel="stylesheet" href="theme.css">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="columns is-gapless sp-signin">
|
||||||
|
<div class="column is-half sp-signin-brand">
|
||||||
|
<div>
|
||||||
|
<img src="../../selfpost-stamp.svg" alt="SelfPost">
|
||||||
|
<p>YOUR SMTP. YOUR RULES.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="column is-half sp-signin-form">
|
||||||
|
<form class="sp-form" method="post" action="overview.html">
|
||||||
|
<p class="sp-kicker">mail.example.org</p>
|
||||||
|
<h1 class="title is-3">Sign in</h1>
|
||||||
|
<div class="notification is-success is-light"><i class="ti ti-circle-check"></i>If that account has a confirmed e-mail, the link is on its way. It works once, for 30 minutes.</div>
|
||||||
|
<div class="field">
|
||||||
|
<label class="label" for="username">Username</label>
|
||||||
|
<div class="control has-icons-left"><input class="input" id="username" name="username" autocomplete="username" required><span class="icon is-left"><i class="ti ti-user"></i></span></div>
|
||||||
|
</div>
|
||||||
|
<div class="field">
|
||||||
|
<label class="label" for="password">Password</label>
|
||||||
|
<div class="control has-icons-left"><input class="input" id="password" name="password" type="password" autocomplete="current-password" required><span class="icon is-left"><i class="ti ti-lock"></i></span></div>
|
||||||
|
</div>
|
||||||
|
<div class="field mt-5"><button type="submit" class="button is-primary is-fullwidth">Sign in</button></div>
|
||||||
|
<p class="mt-4"><a href="login-reset.html">Forgot your password?</a></p>
|
||||||
|
</form>
|
||||||
|
<p class="sp-foot">Copyright © 2026 Mikhail Yenuchenko · <a href="#">License (AGPL-3.0)</a> · <a href="#">Source</a> · No warranty</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en" data-theme="light">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
|
<title>New password · SelfPost</title>
|
||||||
|
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bulma@1.0.4/css/bulma.min.css">
|
||||||
|
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@tabler/icons-webfont@3/dist/tabler-icons.min.css">
|
||||||
|
<link rel="stylesheet" href="../shared/brand.css">
|
||||||
|
<link rel="stylesheet" href="theme.css">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="columns is-gapless sp-signin">
|
||||||
|
<div class="column is-half sp-signin-brand">
|
||||||
|
<div>
|
||||||
|
<img src="../../selfpost-stamp.svg" alt="SelfPost">
|
||||||
|
<p>YOUR SMTP. YOUR RULES.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="column is-half sp-signin-form">
|
||||||
|
<form class="sp-form" method="post" action="login.html">
|
||||||
|
<p class="sp-kicker">mail.example.org · admin</p>
|
||||||
|
<h1 class="title is-3">New password</h1>
|
||||||
|
<p class="sp-muted mb-4">Setting it signs every session of this account out; sign in with it afterwards.</p>
|
||||||
|
<div class="field">
|
||||||
|
<label class="label" for="new_password">New password</label>
|
||||||
|
<div class="control has-icons-left"><input class="input" id="new_password" name="new_password" type="password" autocomplete="new-password" required><span class="icon is-left"><i class="ti ti-lock"></i></span></div>
|
||||||
|
</div>
|
||||||
|
<div class="field">
|
||||||
|
<label class="label" for="new_password_confirm">Repeat</label>
|
||||||
|
<div class="control has-icons-left"><input class="input" id="new_password_confirm" name="new_password_confirm" type="password" autocomplete="new-password" required><span class="icon is-left"><i class="ti ti-lock"></i></span></div>
|
||||||
|
</div>
|
||||||
|
<div class="field mt-5"><button type="submit" class="button is-primary is-fullwidth">Set password</button></div>
|
||||||
|
</form>
|
||||||
|
<p class="sp-foot">Copyright © 2026 Mikhail Yenuchenko · <a href="#">License (AGPL-3.0)</a> · <a href="#">Source</a> · No warranty</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en" data-theme="light">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
|
<title>Reset password · SelfPost</title>
|
||||||
|
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bulma@1.0.4/css/bulma.min.css">
|
||||||
|
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@tabler/icons-webfont@3/dist/tabler-icons.min.css">
|
||||||
|
<link rel="stylesheet" href="../shared/brand.css">
|
||||||
|
<link rel="stylesheet" href="theme.css">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="columns is-gapless sp-signin">
|
||||||
|
<div class="column is-half sp-signin-brand">
|
||||||
|
<div>
|
||||||
|
<img src="../../selfpost-stamp.svg" alt="SelfPost">
|
||||||
|
<p>YOUR SMTP. YOUR RULES.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="column is-half sp-signin-form">
|
||||||
|
<form class="sp-form" method="post" action="login-reset-sent.html">
|
||||||
|
<p class="sp-kicker">mail.example.org</p>
|
||||||
|
<h1 class="title is-3">Reset password</h1>
|
||||||
|
<p class="sp-muted mb-4">A link to set a new password goes to the e-mail of the account, once that address is confirmed under Account. It works once, for 30 minutes.</p>
|
||||||
|
<div class="field">
|
||||||
|
<label class="label" for="username">Username</label>
|
||||||
|
<div class="control has-icons-left"><input class="input" id="username" name="username" autocomplete="username" required><span class="icon is-left"><i class="ti ti-user"></i></span></div>
|
||||||
|
</div>
|
||||||
|
<div class="field mt-5"><button type="submit" class="button is-primary is-fullwidth">Send the link</button></div>
|
||||||
|
<p class="mt-4"><a href="login.html">Back to sign in</a></p>
|
||||||
|
</form>
|
||||||
|
<p class="sp-foot">Copyright © 2026 Mikhail Yenuchenko · <a href="#">License (AGPL-3.0)</a> · <a href="#">Source</a> · No warranty</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -30,8 +30,9 @@
|
|||||||
<div class="control has-icons-left"><input class="input" id="password" name="password" type="password" autocomplete="current-password" required><span class="icon is-left"><i class="ti ti-lock"></i></span></div>
|
<div class="control has-icons-left"><input class="input" id="password" name="password" type="password" autocomplete="current-password" required><span class="icon is-left"><i class="ti ti-lock"></i></span></div>
|
||||||
</div>
|
</div>
|
||||||
<div class="field mt-5"><button type="submit" class="button is-primary is-fullwidth">Sign in</button></div>
|
<div class="field mt-5"><button type="submit" class="button is-primary is-fullwidth">Sign in</button></div>
|
||||||
|
<p class="mt-4"><a href="login-reset.html">Forgot your password?</a></p>
|
||||||
</form>
|
</form>
|
||||||
<p class="sp-foot">© 2026 Mix · <a href="#">License (AGPL-3.0)</a> · <a href="#">Source</a> · No warranty</p>
|
<p class="sp-foot">Copyright © 2026 Mikhail Yenuchenko · <a href="#">License (AGPL-3.0)</a> · <a href="#">Source</a> · No warranty</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -12,8 +12,8 @@
|
|||||||
<body>
|
<body>
|
||||||
<nav class="navbar is-primary" aria-label="main navigation">
|
<nav class="navbar is-primary" aria-label="main navigation">
|
||||||
<div class="container">
|
<div class="container">
|
||||||
<div class="navbar-brand"><a class="navbar-item" href="overview.html"><img src="../../selfpost-wordmark.svg" alt="SelfPost"></a></div>
|
<div class="navbar-brand"><a class="navbar-item" href="overview.html"><img src="../../selfpost-wordmark.svg" alt="SelfPost"></a><details class="sp-touchnav"><summary>Menu</summary><div class="sp-touchnav-panel"><a href="overview.html">Overview</a><details class="sp-touchnav-group" open><summary>Outbound</summary><a href="out-domains.html" aria-current="page"><i class="ti ti-world-upload"></i>Domains</a><a href="out-log.html"><i class="ti ti-list-details"></i>Log</a><a href="out-queue.html"><i class="ti ti-stack-2"></i>Queue</a><a href="dmarc.html"><i class="ti ti-report-analytics"></i>DMARC reports</a></details><details class="sp-touchnav-group"><summary>Inbound</summary><a href="in-domains.html"><i class="ti ti-world-download"></i>Domains</a><a href="in-log.html"><i class="ti ti-list-details"></i>Log</a><a href="in-queue.html"><i class="ti ti-stack-2"></i>Queue</a><a href="in-quarantine.html"><i class="ti ti-mail-pause"></i>Quarantine</a><a href="in-filter-lists.html"><i class="ti ti-filter"></i>Filter lists</a></details><details class="sp-touchnav-group"><summary>Server</summary><a href="health.html"><i class="ti ti-heartbeat"></i>Health</a><a href="preflight.html"><i class="ti ti-checklist"></i>Preflight</a><a href="system-log.html"><i class="ti ti-file-text"></i>System log</a><a href="backup.html"><i class="ti ti-archive"></i>Backup</a><a href="users.html"><i class="ti ti-users"></i>Users</a><a href="settings.html"><i class="ti ti-settings"></i>Settings</a></details><details class="sp-touchnav-group"><summary><i class="ti ti-user-circle"></i>admin</summary><a href="account.html"><i class="ti ti-key"></i>Account</a><a href="help.html"><i class="ti ti-help"></i>Help</a><form method="post" action="login.html"><button type="submit"><i class="ti ti-logout"></i>Sign out</button></form></details></div></details></div>
|
||||||
<div class="navbar-menu is-active">
|
<div class="navbar-menu">
|
||||||
<div class="navbar-start">
|
<div class="navbar-start">
|
||||||
<a class="navbar-item" href="overview.html">Overview</a>
|
<a class="navbar-item" href="overview.html">Overview</a>
|
||||||
<div class="navbar-item has-dropdown is-hoverable sp-current">
|
<div class="navbar-item has-dropdown is-hoverable sp-current">
|
||||||
@@ -100,7 +100,7 @@
|
|||||||
</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 · Copyright © 2026 Mikhail Yenuchenko · <a href="#">License (AGPL-3.0)</a> · <a href="#">Source</a> · No warranty</footer>
|
||||||
</div>
|
</div>
|
||||||
</main>
|
</main>
|
||||||
</body>
|
</body>
|
||||||
|
|||||||
@@ -12,8 +12,8 @@
|
|||||||
<body>
|
<body>
|
||||||
<nav class="navbar is-primary" aria-label="main navigation">
|
<nav class="navbar is-primary" aria-label="main navigation">
|
||||||
<div class="container">
|
<div class="container">
|
||||||
<div class="navbar-brand"><a class="navbar-item" href="overview.html"><img src="../../selfpost-wordmark.svg" alt="SelfPost"></a></div>
|
<div class="navbar-brand"><a class="navbar-item" href="overview.html"><img src="../../selfpost-wordmark.svg" alt="SelfPost"></a><details class="sp-touchnav"><summary>Menu</summary><div class="sp-touchnav-panel"><a href="overview.html">Overview</a><details class="sp-touchnav-group" open><summary>Outbound</summary><a href="out-domains.html" aria-current="page"><i class="ti ti-world-upload"></i>Domains</a><a href="out-log.html"><i class="ti ti-list-details"></i>Log</a><a href="out-queue.html"><i class="ti ti-stack-2"></i>Queue</a><a href="dmarc.html"><i class="ti ti-report-analytics"></i>DMARC reports</a></details><details class="sp-touchnav-group"><summary>Inbound</summary><a href="in-domains.html"><i class="ti ti-world-download"></i>Domains</a><a href="in-log.html"><i class="ti ti-list-details"></i>Log</a><a href="in-queue.html"><i class="ti ti-stack-2"></i>Queue</a><a href="in-quarantine.html"><i class="ti ti-mail-pause"></i>Quarantine</a><a href="in-filter-lists.html"><i class="ti ti-filter"></i>Filter lists</a></details><details class="sp-touchnav-group"><summary>Server</summary><a href="health.html"><i class="ti ti-heartbeat"></i>Health</a><a href="preflight.html"><i class="ti ti-checklist"></i>Preflight</a><a href="system-log.html"><i class="ti ti-file-text"></i>System log</a><a href="backup.html"><i class="ti ti-archive"></i>Backup</a><a href="users.html"><i class="ti ti-users"></i>Users</a><a href="settings.html"><i class="ti ti-settings"></i>Settings</a></details><details class="sp-touchnav-group"><summary><i class="ti ti-user-circle"></i>admin</summary><a href="account.html"><i class="ti ti-key"></i>Account</a><a href="help.html"><i class="ti ti-help"></i>Help</a><form method="post" action="login.html"><button type="submit"><i class="ti ti-logout"></i>Sign out</button></form></details></div></details></div>
|
||||||
<div class="navbar-menu is-active">
|
<div class="navbar-menu">
|
||||||
<div class="navbar-start">
|
<div class="navbar-start">
|
||||||
<a class="navbar-item" href="overview.html">Overview</a>
|
<a class="navbar-item" href="overview.html">Overview</a>
|
||||||
<div class="navbar-item has-dropdown is-hoverable sp-current">
|
<div class="navbar-item has-dropdown is-hoverable sp-current">
|
||||||
@@ -69,7 +69,7 @@
|
|||||||
<div class="sp-grow">
|
<div class="sp-grow">
|
||||||
<p class="sp-kicker"><a href="out-domains.html">Outbound domains</a> / <a href="out-domain.html">example.org</a> / Applications /</p>
|
<p class="sp-kicker"><a href="out-domains.html">Outbound domains</a> / <a href="out-domain.html">example.org</a> / Applications /</p>
|
||||||
<h1 class="title is-3 sp-mono">prod-server</h1>
|
<h1 class="title is-3 sp-mono">prod-server</h1>
|
||||||
<p class="sp-lead">1 102 messages in 30 days · peak 96 msg/h · average 1.5 msg/h. The same form, empty, adds a new application.</p>
|
<p class="sp-lead">1 102 messages in 30 days · peak 96 msg/h · average 1.5 msg/h.</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="buttons">
|
<div class="buttons">
|
||||||
<a class="button" href="out-app-created.html"><i class="ti ti-key"></i>New password</a>
|
<a class="button" href="out-app-created.html"><i class="ti ti-key"></i>New password</a>
|
||||||
@@ -85,7 +85,7 @@
|
|||||||
<div class="sp-box-body">
|
<div class="sp-box-body">
|
||||||
<div class="field"><label class="label" for="login">Login</label>
|
<div class="field"><label class="label" for="login">Login</label>
|
||||||
<div class="control"><input class="input sp-mono" id="login" name="login" value="prod-server" autocomplete="off" spellcheck="false" required></div>
|
<div class="control"><input class="input sp-mono" id="login" name="login" value="prod-server" autocomplete="off" spellcheck="false" required></div>
|
||||||
<p class="help">Unique across domains; letters, digits, <code>.</code> <code>-</code> <code>_</code>.</p></div>
|
<p class="help">Fixed: add a new application to use another login.</p></div>
|
||||||
<div class="field"><label class="label" for="amode">May send as</label>
|
<div class="field"><label class="label" for="amode">May send as</label>
|
||||||
<div class="control"><div class="select is-fullwidth"><select id="amode" name="mode"><option selected>Any address of the domain</option><option>Specific addresses (list)</option></select></div></div></div>
|
<div class="control"><div class="select is-fullwidth"><select id="amode" name="mode"><option selected>Any address of the domain</option><option>Specific addresses (list)</option></select></div></div></div>
|
||||||
<div class="field"><label class="label" for="addresses">Addresses</label>
|
<div class="field"><label class="label" for="addresses">Addresses</label>
|
||||||
@@ -129,7 +129,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
<footer class="sp-foot">SelfPost 1.8.0 · © 2026 Mix · <a href="#">License (AGPL-3.0)</a> · <a href="#">Source</a> · No warranty</footer>
|
<footer class="sp-foot">SelfPost 1.8.0 · Copyright © 2026 Mikhail Yenuchenko · <a href="#">License (AGPL-3.0)</a> · <a href="#">Source</a> · No warranty</footer>
|
||||||
</div>
|
</div>
|
||||||
</main>
|
</main>
|
||||||
</body>
|
</body>
|
||||||
|
|||||||
@@ -12,8 +12,8 @@
|
|||||||
<body>
|
<body>
|
||||||
<nav class="navbar is-primary" aria-label="main navigation">
|
<nav class="navbar is-primary" aria-label="main navigation">
|
||||||
<div class="container">
|
<div class="container">
|
||||||
<div class="navbar-brand"><a class="navbar-item" href="overview.html"><img src="../../selfpost-wordmark.svg" alt="SelfPost"></a></div>
|
<div class="navbar-brand"><a class="navbar-item" href="overview.html"><img src="../../selfpost-wordmark.svg" alt="SelfPost"></a><details class="sp-touchnav"><summary>Menu</summary><div class="sp-touchnav-panel"><a href="overview.html">Overview</a><details class="sp-touchnav-group" open><summary>Outbound</summary><a href="out-domains.html" aria-current="page"><i class="ti ti-world-upload"></i>Domains</a><a href="out-log.html"><i class="ti ti-list-details"></i>Log</a><a href="out-queue.html"><i class="ti ti-stack-2"></i>Queue</a><a href="dmarc.html"><i class="ti ti-report-analytics"></i>DMARC reports</a></details><details class="sp-touchnav-group"><summary>Inbound</summary><a href="in-domains.html"><i class="ti ti-world-download"></i>Domains</a><a href="in-log.html"><i class="ti ti-list-details"></i>Log</a><a href="in-queue.html"><i class="ti ti-stack-2"></i>Queue</a><a href="in-quarantine.html"><i class="ti ti-mail-pause"></i>Quarantine</a><a href="in-filter-lists.html"><i class="ti ti-filter"></i>Filter lists</a></details><details class="sp-touchnav-group"><summary>Server</summary><a href="health.html"><i class="ti ti-heartbeat"></i>Health</a><a href="preflight.html"><i class="ti ti-checklist"></i>Preflight</a><a href="system-log.html"><i class="ti ti-file-text"></i>System log</a><a href="backup.html"><i class="ti ti-archive"></i>Backup</a><a href="users.html"><i class="ti ti-users"></i>Users</a><a href="settings.html"><i class="ti ti-settings"></i>Settings</a></details><details class="sp-touchnav-group"><summary><i class="ti ti-user-circle"></i>admin</summary><a href="account.html"><i class="ti ti-key"></i>Account</a><a href="help.html"><i class="ti ti-help"></i>Help</a><form method="post" action="login.html"><button type="submit"><i class="ti ti-logout"></i>Sign out</button></form></details></div></details></div>
|
||||||
<div class="navbar-menu is-active">
|
<div class="navbar-menu">
|
||||||
<div class="navbar-start">
|
<div class="navbar-start">
|
||||||
<a class="navbar-item" href="overview.html">Overview</a>
|
<a class="navbar-item" href="overview.html">Overview</a>
|
||||||
<div class="navbar-item has-dropdown is-hoverable sp-current">
|
<div class="navbar-item has-dropdown is-hoverable sp-current">
|
||||||
@@ -103,7 +103,7 @@
|
|||||||
</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 · Copyright © 2026 Mikhail Yenuchenko · <a href="#">License (AGPL-3.0)</a> · <a href="#">Source</a> · No warranty</footer>
|
||||||
</div>
|
</div>
|
||||||
</main>
|
</main>
|
||||||
</body>
|
</body>
|
||||||
|
|||||||
@@ -12,8 +12,8 @@
|
|||||||
<body>
|
<body>
|
||||||
<nav class="navbar is-primary" aria-label="main navigation">
|
<nav class="navbar is-primary" aria-label="main navigation">
|
||||||
<div class="container">
|
<div class="container">
|
||||||
<div class="navbar-brand"><a class="navbar-item" href="overview.html"><img src="../../selfpost-wordmark.svg" alt="SelfPost"></a></div>
|
<div class="navbar-brand"><a class="navbar-item" href="overview.html"><img src="../../selfpost-wordmark.svg" alt="SelfPost"></a><details class="sp-touchnav"><summary>Menu</summary><div class="sp-touchnav-panel"><a href="overview.html">Overview</a><details class="sp-touchnav-group" open><summary>Outbound</summary><a href="out-domains.html" aria-current="page"><i class="ti ti-world-upload"></i>Domains</a><a href="out-log.html"><i class="ti ti-list-details"></i>Log</a><a href="out-queue.html"><i class="ti ti-stack-2"></i>Queue</a><a href="dmarc.html"><i class="ti ti-report-analytics"></i>DMARC reports</a></details><details class="sp-touchnav-group"><summary>Inbound</summary><a href="in-domains.html"><i class="ti ti-world-download"></i>Domains</a><a href="in-log.html"><i class="ti ti-list-details"></i>Log</a><a href="in-queue.html"><i class="ti ti-stack-2"></i>Queue</a><a href="in-quarantine.html"><i class="ti ti-mail-pause"></i>Quarantine</a><a href="in-filter-lists.html"><i class="ti ti-filter"></i>Filter lists</a></details><details class="sp-touchnav-group"><summary>Server</summary><a href="health.html"><i class="ti ti-heartbeat"></i>Health</a><a href="preflight.html"><i class="ti ti-checklist"></i>Preflight</a><a href="system-log.html"><i class="ti ti-file-text"></i>System log</a><a href="backup.html"><i class="ti ti-archive"></i>Backup</a><a href="users.html"><i class="ti ti-users"></i>Users</a><a href="settings.html"><i class="ti ti-settings"></i>Settings</a></details><details class="sp-touchnav-group"><summary><i class="ti ti-user-circle"></i>admin</summary><a href="account.html"><i class="ti ti-key"></i>Account</a><a href="help.html"><i class="ti ti-help"></i>Help</a><form method="post" action="login.html"><button type="submit"><i class="ti ti-logout"></i>Sign out</button></form></details></div></details></div>
|
||||||
<div class="navbar-menu is-active">
|
<div class="navbar-menu">
|
||||||
<div class="navbar-start">
|
<div class="navbar-start">
|
||||||
<a class="navbar-item" href="overview.html">Overview</a>
|
<a class="navbar-item" href="overview.html">Overview</a>
|
||||||
<div class="navbar-item has-dropdown is-hoverable sp-current">
|
<div class="navbar-item has-dropdown is-hoverable sp-current">
|
||||||
@@ -104,6 +104,8 @@
|
|||||||
<div class="field"><label class="label" for="encpw2">Repeat password</label><div class="control"><input class="input" id="encpw2" type="password"></div></div>
|
<div class="field"><label class="label" for="encpw2">Repeat password</label><div class="control"><input class="input" id="encpw2" type="password"></div></div>
|
||||||
</div>
|
</div>
|
||||||
<p class="help mb-3">Keep this password: SelfPost does not store it, and the file cannot be opened without it.</p>
|
<p class="help mb-3">Keep this password: SelfPost does not store it, and the file cannot be opened without it.</p>
|
||||||
|
<div class="field"><label class="label" for="acpw">Your account password</label><div class="control"><input class="input" id="acpw" name="account_password" type="password" autocomplete="current-password" required></div>
|
||||||
|
<p class="help">The one you sign in with, asked at every export: the file holds the domain's DKIM key and its applications' credentials.</p></div>
|
||||||
<button type="submit" class="button"><i class="ti ti-download"></i>Export domain</button>
|
<button type="submit" class="button"><i class="ti ti-download"></i>Export domain</button>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
@@ -117,9 +119,9 @@
|
|||||||
<form class="sp-form" method="post" action="#">
|
<form class="sp-form" method="post" action="#">
|
||||||
<div class="field"><label class="label" for="mode">Limit mode</label>
|
<div class="field"><label class="label" for="mode">Limit mode</label>
|
||||||
<div class="control"><div class="select is-fullwidth"><select id="mode" name="mode"><option>Manual</option><option selected>Auto (from statistics)</option></select></div></div></div>
|
<div class="control"><div class="select is-fullwidth"><select id="mode" name="mode"><option>Manual</option><option selected>Auto (from statistics)</option></select></div></div></div>
|
||||||
<div class="field"><label class="label" for="mult">Multiplier (1.5–10, default 3)</label>
|
<div class="field"><label class="label" for="mult">Multiplier (1.5–5, default 2.5)</label>
|
||||||
<div class="control"><input class="input" id="mult" name="auto_multiplier" type="number" min="1.5" max="10" step="0.1" value="3"></div>
|
<div class="control"><input class="input" id="mult" name="auto_multiplier" type="number" min="1.5" max="10" step="0.1" value="3"></div>
|
||||||
<p class="help">Computed limit: <strong>288</strong> messages / 3600 s. Peak was 96 msg/h. Last recalculated yesterday 03:00 UTC.</p></div>
|
<p class="help">Computed limit: <strong>288</strong> messages / 3600 s. Peak was 96 msg/h. Last recalculated yesterday 03:00.</p></div>
|
||||||
<div class="buttons">
|
<div class="buttons">
|
||||||
<button type="submit" class="button is-primary">Save limit</button>
|
<button type="submit" class="button is-primary">Save limit</button>
|
||||||
<button type="submit" class="button" formaction="#recalc">Recalculate now</button>
|
<button type="submit" class="button" formaction="#recalc">Recalculate now</button>
|
||||||
@@ -140,7 +142,7 @@
|
|||||||
</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 · Copyright © 2026 Mikhail Yenuchenko · <a href="#">License (AGPL-3.0)</a> · <a href="#">Source</a> · No warranty</footer>
|
||||||
</div>
|
</div>
|
||||||
</main>
|
</main>
|
||||||
</body>
|
</body>
|
||||||
|
|||||||
@@ -12,8 +12,8 @@
|
|||||||
<body>
|
<body>
|
||||||
<nav class="navbar is-primary" aria-label="main navigation">
|
<nav class="navbar is-primary" aria-label="main navigation">
|
||||||
<div class="container">
|
<div class="container">
|
||||||
<div class="navbar-brand"><a class="navbar-item" href="overview.html"><img src="../../selfpost-wordmark.svg" alt="SelfPost"></a></div>
|
<div class="navbar-brand"><a class="navbar-item" href="overview.html"><img src="../../selfpost-wordmark.svg" alt="SelfPost"></a><details class="sp-touchnav"><summary>Menu</summary><div class="sp-touchnav-panel"><a href="overview.html">Overview</a><details class="sp-touchnav-group" open><summary>Outbound</summary><a href="out-domains.html" aria-current="page"><i class="ti ti-world-upload"></i>Domains</a><a href="out-log.html"><i class="ti ti-list-details"></i>Log</a><a href="out-queue.html"><i class="ti ti-stack-2"></i>Queue</a><a href="dmarc.html"><i class="ti ti-report-analytics"></i>DMARC reports</a></details><details class="sp-touchnav-group"><summary>Inbound</summary><a href="in-domains.html"><i class="ti ti-world-download"></i>Domains</a><a href="in-log.html"><i class="ti ti-list-details"></i>Log</a><a href="in-queue.html"><i class="ti ti-stack-2"></i>Queue</a><a href="in-quarantine.html"><i class="ti ti-mail-pause"></i>Quarantine</a><a href="in-filter-lists.html"><i class="ti ti-filter"></i>Filter lists</a></details><details class="sp-touchnav-group"><summary>Server</summary><a href="health.html"><i class="ti ti-heartbeat"></i>Health</a><a href="preflight.html"><i class="ti ti-checklist"></i>Preflight</a><a href="system-log.html"><i class="ti ti-file-text"></i>System log</a><a href="backup.html"><i class="ti ti-archive"></i>Backup</a><a href="users.html"><i class="ti ti-users"></i>Users</a><a href="settings.html"><i class="ti ti-settings"></i>Settings</a></details><details class="sp-touchnav-group"><summary><i class="ti ti-user-circle"></i>admin</summary><a href="account.html"><i class="ti ti-key"></i>Account</a><a href="help.html"><i class="ti ti-help"></i>Help</a><form method="post" action="login.html"><button type="submit"><i class="ti ti-logout"></i>Sign out</button></form></details></div></details></div>
|
||||||
<div class="navbar-menu is-active">
|
<div class="navbar-menu">
|
||||||
<div class="navbar-start">
|
<div class="navbar-start">
|
||||||
<a class="navbar-item" href="overview.html">Overview</a>
|
<a class="navbar-item" href="overview.html">Overview</a>
|
||||||
<div class="navbar-item has-dropdown is-hoverable sp-current">
|
<div class="navbar-item has-dropdown is-hoverable sp-current">
|
||||||
@@ -106,8 +106,8 @@
|
|||||||
<div class="box" id="dns">
|
<div class="box" id="dns">
|
||||||
<div class="sp-box-head"><span class="sp-no">01</span><h2>DNS records</h2><div class="sp-end">Checked 3 min ago</div><a class="sp-help" href="help.html#dns" title="What to publish and why"><i class="ti ti-help-circle"></i></a></div>
|
<div class="sp-box-head"><span class="sp-no">01</span><h2>DNS records</h2><div class="sp-end">Checked 3 min ago</div><a class="sp-help" href="help.html#dns" title="What to publish and why"><i class="ti ti-help-circle"></i></a></div>
|
||||||
|
|
||||||
<div class="sp-record">
|
<details class="sp-record">
|
||||||
<div class="sp-record-head"><h3>DKIM</h3><span class="tag is-success is-light">published</span><small>Not a secret · selector <code>sp2026</code></small></div>
|
<summary class="sp-record-head"><h3>DKIM</h3><span class="tag is-success is-light">published</span><small>Not a secret · selector <code>sp2026</code></small><span class="sp-record-toggle"><i class="ti ti-eye"></i><span>Show values</span><span>Hide values</span></span></summary>
|
||||||
<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="sp2026._domainkey.example.org"></div><div class="control"><button type="button" class="button"><i class="ti ti-copy"></i>Copy</button></div></div></div>
|
<div class="field has-addons"><div class="control is-expanded"><input class="input" readonly value="sp2026._domainkey.example.org"></div><div class="control"><button type="button" class="button"><i class="ti ti-copy"></i>Copy</button></div></div></div>
|
||||||
@@ -115,7 +115,7 @@
|
|||||||
</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"><textarea class="textarea" rows="3" readonly>v=DKIM1; k=rsa; p=MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAx3kqV0mJd8e2f1Yb7Qw5uT9sLr4nHc6pZaXo1vKe0yWm2gBt8RjN5dCq3hUf7lPs9aEi4oMz6xTb1kGv0nYw…IDAQAB</textarea></div><div class="control"><button type="button" class="button"><i class="ti ti-copy"></i>Copy</button></div></div></div>
|
<div class="field has-addons"><div class="control is-expanded"><textarea class="textarea" rows="3" readonly>v=DKIM1; k=rsa; p=MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAx3kqV0mJd8e2f1Yb7Qw5uT9sLr4nHc6pZaXo1vKe0yWm2gBt8RjN5dCq3hUf7lPs9aEi4oMz6xTb1kGv0nYw…IDAQAB</textarea></div><div class="control"><button type="button" class="button"><i class="ti ti-copy"></i>Copy</button></div></div></div>
|
||||||
</div>
|
</details>
|
||||||
|
|
||||||
<div class="sp-record">
|
<div class="sp-record">
|
||||||
<div class="sp-record-head"><h3>SPF</h3><span class="tag is-danger is-light">mismatch</span><small>Merge into the existing record — never publish a second one</small></div>
|
<div class="sp-record-head"><h3>SPF</h3><span class="tag is-danger is-light">mismatch</span><small>Merge into the existing record — never publish a second one</small></div>
|
||||||
@@ -131,8 +131,8 @@
|
|||||||
<p class="help is-danger">The published record does not authorize 203.0.113.25. Add <code>ip4:203.0.113.25</code> before <code>~all</code>. Shallow check: <code>include:</code> is not followed.</p></div>
|
<p class="help is-danger">The published record does not authorize 203.0.113.25. Add <code>ip4:203.0.113.25</code> before <code>~all</code>. Shallow check: <code>include:</code> is not followed.</p></div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="sp-record">
|
<details class="sp-record">
|
||||||
<div class="sp-record-head"><h3>DMARC</h3><span class="tag is-success is-light">published</span><small>Report address: <a href="out-domain-settings.html">example.org@dmarc.mail.example.org</a></small></div>
|
<summary 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><span class="sp-record-toggle"><i class="ti ti-eye"></i><span>Show values</span><span>Hide values</span></span></summary>
|
||||||
<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>
|
||||||
@@ -141,7 +141,7 @@
|
|||||||
<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: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>
|
<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>
|
</details>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="box" id="apps">
|
<div class="box" id="apps">
|
||||||
@@ -172,7 +172,7 @@
|
|||||||
</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 · Copyright © 2026 Mikhail Yenuchenko · <a href="#">License (AGPL-3.0)</a> · <a href="#">Source</a> · No warranty</footer>
|
||||||
</div>
|
</div>
|
||||||
</main>
|
</main>
|
||||||
</body>
|
</body>
|
||||||
|
|||||||
@@ -12,8 +12,8 @@
|
|||||||
<body>
|
<body>
|
||||||
<nav class="navbar is-primary" aria-label="main navigation">
|
<nav class="navbar is-primary" aria-label="main navigation">
|
||||||
<div class="container">
|
<div class="container">
|
||||||
<div class="navbar-brand"><a class="navbar-item" href="overview.html"><img src="../../selfpost-wordmark.svg" alt="SelfPost"></a></div>
|
<div class="navbar-brand"><a class="navbar-item" href="overview.html"><img src="../../selfpost-wordmark.svg" alt="SelfPost"></a><details class="sp-touchnav"><summary>Menu</summary><div class="sp-touchnav-panel"><a href="overview.html">Overview</a><details class="sp-touchnav-group" open><summary>Outbound</summary><a href="out-domains.html" aria-current="page"><i class="ti ti-world-upload"></i>Domains</a><a href="out-log.html"><i class="ti ti-list-details"></i>Log</a><a href="out-queue.html"><i class="ti ti-stack-2"></i>Queue</a><a href="dmarc.html"><i class="ti ti-report-analytics"></i>DMARC reports</a></details><details class="sp-touchnav-group"><summary>Inbound</summary><a href="in-domains.html"><i class="ti ti-world-download"></i>Domains</a><a href="in-log.html"><i class="ti ti-list-details"></i>Log</a><a href="in-queue.html"><i class="ti ti-stack-2"></i>Queue</a><a href="in-quarantine.html"><i class="ti ti-mail-pause"></i>Quarantine</a><a href="in-filter-lists.html"><i class="ti ti-filter"></i>Filter lists</a></details><details class="sp-touchnav-group"><summary>Server</summary><a href="health.html"><i class="ti ti-heartbeat"></i>Health</a><a href="preflight.html"><i class="ti ti-checklist"></i>Preflight</a><a href="system-log.html"><i class="ti ti-file-text"></i>System log</a><a href="backup.html"><i class="ti ti-archive"></i>Backup</a><a href="users.html"><i class="ti ti-users"></i>Users</a><a href="settings.html"><i class="ti ti-settings"></i>Settings</a></details><details class="sp-touchnav-group"><summary><i class="ti ti-user-circle"></i>admin</summary><a href="account.html"><i class="ti ti-key"></i>Account</a><a href="help.html"><i class="ti ti-help"></i>Help</a><form method="post" action="login.html"><button type="submit"><i class="ti ti-logout"></i>Sign out</button></form></details></div></details></div>
|
||||||
<div class="navbar-menu is-active">
|
<div class="navbar-menu">
|
||||||
<div class="navbar-start">
|
<div class="navbar-start">
|
||||||
<a class="navbar-item" href="overview.html">Overview</a>
|
<a class="navbar-item" href="overview.html">Overview</a>
|
||||||
<div class="navbar-item has-dropdown is-hoverable sp-current">
|
<div class="navbar-item has-dropdown is-hoverable sp-current">
|
||||||
@@ -100,7 +100,7 @@
|
|||||||
<div class="sp-box-foot">Each tag is one check. Green — published and correct; amber — published but weak; red — missing or wrong.</div>
|
<div class="sp-box-foot">Each tag is one check. Green — published and correct; amber — published but weak; red — missing or wrong.</div>
|
||||||
</div>
|
</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 · Copyright © 2026 Mikhail Yenuchenko · <a href="#">License (AGPL-3.0)</a> · <a href="#">Source</a> · No warranty</footer>
|
||||||
</div>
|
</div>
|
||||||
</main>
|
</main>
|
||||||
</body>
|
</body>
|
||||||
|
|||||||
@@ -12,8 +12,8 @@
|
|||||||
<body>
|
<body>
|
||||||
<nav class="navbar is-primary" aria-label="main navigation">
|
<nav class="navbar is-primary" aria-label="main navigation">
|
||||||
<div class="container">
|
<div class="container">
|
||||||
<div class="navbar-brand"><a class="navbar-item" href="overview.html"><img src="../../selfpost-wordmark.svg" alt="SelfPost"></a></div>
|
<div class="navbar-brand"><a class="navbar-item" href="overview.html"><img src="../../selfpost-wordmark.svg" alt="SelfPost"></a><details class="sp-touchnav"><summary>Menu</summary><div class="sp-touchnav-panel"><a href="overview.html">Overview</a><details class="sp-touchnav-group" open><summary>Outbound</summary><a href="out-domains.html"><i class="ti ti-world-upload"></i>Domains</a><a href="out-log.html" aria-current="page"><i class="ti ti-list-details"></i>Log</a><a href="out-queue.html"><i class="ti ti-stack-2"></i>Queue</a><a href="dmarc.html"><i class="ti ti-report-analytics"></i>DMARC reports</a></details><details class="sp-touchnav-group"><summary>Inbound</summary><a href="in-domains.html"><i class="ti ti-world-download"></i>Domains</a><a href="in-log.html"><i class="ti ti-list-details"></i>Log</a><a href="in-queue.html"><i class="ti ti-stack-2"></i>Queue</a><a href="in-quarantine.html"><i class="ti ti-mail-pause"></i>Quarantine</a><a href="in-filter-lists.html"><i class="ti ti-filter"></i>Filter lists</a></details><details class="sp-touchnav-group"><summary>Server</summary><a href="health.html"><i class="ti ti-heartbeat"></i>Health</a><a href="preflight.html"><i class="ti ti-checklist"></i>Preflight</a><a href="system-log.html"><i class="ti ti-file-text"></i>System log</a><a href="backup.html"><i class="ti ti-archive"></i>Backup</a><a href="users.html"><i class="ti ti-users"></i>Users</a><a href="settings.html"><i class="ti ti-settings"></i>Settings</a></details><details class="sp-touchnav-group"><summary><i class="ti ti-user-circle"></i>admin</summary><a href="account.html"><i class="ti ti-key"></i>Account</a><a href="help.html"><i class="ti ti-help"></i>Help</a><form method="post" action="login.html"><button type="submit"><i class="ti ti-logout"></i>Sign out</button></form></details></div></details></div>
|
||||||
<div class="navbar-menu is-active">
|
<div class="navbar-menu">
|
||||||
<div class="navbar-start">
|
<div class="navbar-start">
|
||||||
<a class="navbar-item" href="overview.html">Overview</a>
|
<a class="navbar-item" href="overview.html">Overview</a>
|
||||||
<div class="navbar-item has-dropdown is-hoverable sp-current">
|
<div class="navbar-item has-dropdown is-hoverable sp-current">
|
||||||
@@ -87,7 +87,7 @@
|
|||||||
<a class="sp-help" href="help.html#log" title="What the statuses mean"><i class="ti ti-help-circle"></i></a>
|
<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</th><th>From</th><th>To</th><th>Subject</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>shop@<wbr>shop.example.org</td><td>m.keller@<wbr>gmx.de</td><td class="sp-clip">Your order #48213 has shipped</td><td><span class="tag is-success is-light">sent</span></td><td class="sp-actions"><a href="out-message.html">Details</a></td></tr>
|
<tr><td class="sp-mono sp-muted sp-nowrap sp-small">09-21 14:01:52</td><td>shop@<wbr>shop.example.org</td><td>m.keller@<wbr>gmx.de</td><td class="sp-clip">Your order #48213 has shipped</td><td><span class="tag is-success is-light">sent</span></td><td class="sp-actions"><a href="out-message.html">Details</a></td></tr>
|
||||||
<tr><td class="sp-mono sp-muted sp-nowrap sp-small">09-21 14:01:50</td><td>shop@<wbr>shop.example.org</td><td>anna.lind@<wbr>outlook.com</td><td class="sp-clip">Your order #48212 has shipped</td><td><span class="tag is-success is-light">sent</span></td><td class="sp-actions"><a href="out-message.html">Details</a></td></tr>
|
<tr><td class="sp-mono sp-muted sp-nowrap sp-small">09-21 14:01:50</td><td>shop@<wbr>shop.example.org</td><td>anna.lind@<wbr>outlook.com</td><td class="sp-clip">Your order #48212 has shipped</td><td><span class="tag is-success is-light">sent</span></td><td class="sp-actions"><a href="out-message.html">Details</a></td></tr>
|
||||||
@@ -101,7 +101,7 @@
|
|||||||
<div class="sp-box-foot"><span>Page 1 of 412</span><a href="#">Older →</a><span class="sp-end">New rows appear on their own</span></div>
|
<div class="sp-box-foot"><span>Page 1 of 412</span><a href="#">Older →</a><span class="sp-end">New rows appear on their own</span></div>
|
||||||
</div>
|
</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 · Copyright © 2026 Mikhail Yenuchenko · <a href="#">License (AGPL-3.0)</a> · <a href="#">Source</a> · No warranty</footer>
|
||||||
</div>
|
</div>
|
||||||
</main>
|
</main>
|
||||||
</body>
|
</body>
|
||||||
|
|||||||
@@ -12,8 +12,8 @@
|
|||||||
<body>
|
<body>
|
||||||
<nav class="navbar is-primary" aria-label="main navigation">
|
<nav class="navbar is-primary" aria-label="main navigation">
|
||||||
<div class="container">
|
<div class="container">
|
||||||
<div class="navbar-brand"><a class="navbar-item" href="overview.html"><img src="../../selfpost-wordmark.svg" alt="SelfPost"></a></div>
|
<div class="navbar-brand"><a class="navbar-item" href="overview.html"><img src="../../selfpost-wordmark.svg" alt="SelfPost"></a><details class="sp-touchnav"><summary>Menu</summary><div class="sp-touchnav-panel"><a href="overview.html">Overview</a><details class="sp-touchnav-group" open><summary>Outbound</summary><a href="out-domains.html"><i class="ti ti-world-upload"></i>Domains</a><a href="out-log.html" aria-current="page"><i class="ti ti-list-details"></i>Log</a><a href="out-queue.html"><i class="ti ti-stack-2"></i>Queue</a><a href="dmarc.html"><i class="ti ti-report-analytics"></i>DMARC reports</a></details><details class="sp-touchnav-group"><summary>Inbound</summary><a href="in-domains.html"><i class="ti ti-world-download"></i>Domains</a><a href="in-log.html"><i class="ti ti-list-details"></i>Log</a><a href="in-queue.html"><i class="ti ti-stack-2"></i>Queue</a><a href="in-quarantine.html"><i class="ti ti-mail-pause"></i>Quarantine</a><a href="in-filter-lists.html"><i class="ti ti-filter"></i>Filter lists</a></details><details class="sp-touchnav-group"><summary>Server</summary><a href="health.html"><i class="ti ti-heartbeat"></i>Health</a><a href="preflight.html"><i class="ti ti-checklist"></i>Preflight</a><a href="system-log.html"><i class="ti ti-file-text"></i>System log</a><a href="backup.html"><i class="ti ti-archive"></i>Backup</a><a href="users.html"><i class="ti ti-users"></i>Users</a><a href="settings.html"><i class="ti ti-settings"></i>Settings</a></details><details class="sp-touchnav-group"><summary><i class="ti ti-user-circle"></i>admin</summary><a href="account.html"><i class="ti ti-key"></i>Account</a><a href="help.html"><i class="ti ti-help"></i>Help</a><form method="post" action="login.html"><button type="submit"><i class="ti ti-logout"></i>Sign out</button></form></details></div></details></div>
|
||||||
<div class="navbar-menu is-active">
|
<div class="navbar-menu">
|
||||||
<div class="navbar-start">
|
<div class="navbar-start">
|
||||||
<a class="navbar-item" href="overview.html">Overview</a>
|
<a class="navbar-item" href="overview.html">Overview</a>
|
||||||
<div class="navbar-item has-dropdown is-hoverable sp-current">
|
<div class="navbar-item has-dropdown is-hoverable sp-current">
|
||||||
@@ -81,8 +81,8 @@
|
|||||||
<dl class="sp-facts">
|
<dl class="sp-facts">
|
||||||
<div><dt>Domain</dt><dd><a href="out-domain.html">example.org</a></dd></div>
|
<div><dt>Domain</dt><dd><a href="out-domain.html">example.org</a></dd></div>
|
||||||
<div><dt>Application</dt><dd class="sp-mono"><a href="out-app.html">alerts</a></dd></div>
|
<div><dt>Application</dt><dd class="sp-mono"><a href="out-app.html">alerts</a></dd></div>
|
||||||
<div><dt>Accepted</dt><dd>2026-09-21 13:58:07 UTC</dd></div>
|
<div><dt>Accepted</dt><dd>2026-09-21 13:58:07</dd></div>
|
||||||
<div><dt>Status reported</dt><dd>2026-09-21 13:58:09 UTC</dd></div>
|
<div><dt>Status reported</dt><dd>2026-09-21 13:58:09</dd></div>
|
||||||
<div><dt>Queue id</dt><dd class="sp-mono">4XcB7k2Jm9z1</dd></div>
|
<div><dt>Queue id</dt><dd class="sp-mono">4XcB7k2Jm9z1</dd></div>
|
||||||
<div><dt>Journal id</dt><dd class="sp-mono">184220</dd></div>
|
<div><dt>Journal id</dt><dd class="sp-mono">184220</dd></div>
|
||||||
</dl>
|
</dl>
|
||||||
@@ -94,9 +94,9 @@
|
|||||||
<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-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>2026-09-21 13:58:07</time><strong>Accepted and queued</strong> — Postfix accepted the message over an authenticated submission and the journal-milter recorded it.</li>
|
||||||
<li class="sp-warn"><time>13:58:09 UTC</time><strong>Deferred</strong> — the receiving server asked to retry later</li>
|
<li class="sp-warn"><time>2026-09-21 13:58:09</time><strong>Deferred, will be retried</strong> — the receiving server could not take the message yet. Postfix retries: first after 5 minutes, then with increasing gaps up to 1 hour 7 minutes, for up to 5 days.</li>
|
||||||
<li class="sp-pending"><time>next attempt ≈ 14:08 UTC</time>Still in the <a href="out-queue.html">queue</a>; kept for 5 days, then bounced</li>
|
<li class="sp-pending"><time>not yet</time>Still in the queue; the next attempt is Postfix's own, and after 5 days it is bounced.</li>
|
||||||
</ol>
|
</ol>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -105,13 +105,14 @@
|
|||||||
|
|
||||||
<div class="box">
|
<div class="box">
|
||||||
<div class="sp-box-head"><span class="sp-no">03</span><h2>Delivery log</h2><div class="sp-end">Lines from <code>mail.log</code> with this queue id</div></div>
|
<div class="sp-box-head"><span class="sp-no">03</span><h2>Delivery log</h2><div class="sp-end">Lines from <code>mail.log</code> with this queue id</div></div>
|
||||||
<pre class="sp-log"><span class="sp-d">13:58:07</span> postfix/smtpd[2114]: 4XcB7k2Jm9z1: client=unknown[198.51.100.7], sasl_username=alerts
|
<pre class="sp-log"><span class="sp-ll"><span class="sp-d">13:58:07</span> postfix/smtpd[2114]: 4XcB7k2Jm9z1: client=unknown[198.51.100.7], sasl_username=alerts
|
||||||
<span class="sp-d">13:58:07</span> postfix/cleanup[2120]: 4XcB7k2Jm9z1: message-id=<a81f0c@alertmanager>
|
</span><span class="sp-ll"><span class="sp-d">13:58:07</span> postfix/cleanup[2120]: 4XcB7k2Jm9z1: message-id=<a81f0c@alertmanager>
|
||||||
<span class="sp-d">13:58:07</span> postfix/qmgr[2061]: 4XcB7k2Jm9z1: from=<alerts@example.org>, size=2214, nrcpt=1 (queue active)
|
</span><span class="sp-ll"><span class="sp-d">13:58:07</span> postfix/qmgr[2061]: 4XcB7k2Jm9z1: from=<alerts@example.org>, size=2214, nrcpt=1 (queue active)
|
||||||
<span class="sp-d">13:58:09</span> <span class="sp-w">postfix/smtp[2123]: 4XcB7k2Jm9z1: to=<noc@example.org>, relay=mx1.example.org[192.0.2.10]:25, delay=1.9, status=deferred (host mx1.example.org said: 451 4.7.1 Greylisted, try again in 5 minutes)</span></pre>
|
</span><span class="sp-ll"><span class="sp-d">13:58:09</span> <span class="sp-w">postfix/smtp[2123]: 4XcB7k2Jm9z1: to=<noc@example.org>, relay=mx1.example.org[192.0.2.10]:25, delay=1.9, status=deferred (host mx1.example.org said: 451 4.7.1 Greylisted, try again in 5 minutes)</span>
|
||||||
|
</span></pre>
|
||||||
</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 · Copyright © 2026 Mikhail Yenuchenko · <a href="#">License (AGPL-3.0)</a> · <a href="#">Source</a> · No warranty</footer>
|
||||||
</div>
|
</div>
|
||||||
</main>
|
</main>
|
||||||
</body>
|
</body>
|
||||||
|
|||||||
@@ -12,8 +12,8 @@
|
|||||||
<body>
|
<body>
|
||||||
<nav class="navbar is-primary" aria-label="main navigation">
|
<nav class="navbar is-primary" aria-label="main navigation">
|
||||||
<div class="container">
|
<div class="container">
|
||||||
<div class="navbar-brand"><a class="navbar-item" href="overview.html"><img src="../../selfpost-wordmark.svg" alt="SelfPost"></a></div>
|
<div class="navbar-brand"><a class="navbar-item" href="overview.html"><img src="../../selfpost-wordmark.svg" alt="SelfPost"></a><details class="sp-touchnav"><summary>Menu</summary><div class="sp-touchnav-panel"><a href="overview.html">Overview</a><details class="sp-touchnav-group" open><summary>Outbound</summary><a href="out-domains.html"><i class="ti ti-world-upload"></i>Domains</a><a href="out-log.html"><i class="ti ti-list-details"></i>Log</a><a href="out-queue.html" aria-current="page"><i class="ti ti-stack-2"></i>Queue</a><a href="dmarc.html"><i class="ti ti-report-analytics"></i>DMARC reports</a></details><details class="sp-touchnav-group"><summary>Inbound</summary><a href="in-domains.html"><i class="ti ti-world-download"></i>Domains</a><a href="in-log.html"><i class="ti ti-list-details"></i>Log</a><a href="in-queue.html"><i class="ti ti-stack-2"></i>Queue</a><a href="in-quarantine.html"><i class="ti ti-mail-pause"></i>Quarantine</a><a href="in-filter-lists.html"><i class="ti ti-filter"></i>Filter lists</a></details><details class="sp-touchnav-group"><summary>Server</summary><a href="health.html"><i class="ti ti-heartbeat"></i>Health</a><a href="preflight.html"><i class="ti ti-checklist"></i>Preflight</a><a href="system-log.html"><i class="ti ti-file-text"></i>System log</a><a href="backup.html"><i class="ti ti-archive"></i>Backup</a><a href="users.html"><i class="ti ti-users"></i>Users</a><a href="settings.html"><i class="ti ti-settings"></i>Settings</a></details><details class="sp-touchnav-group"><summary><i class="ti ti-user-circle"></i>admin</summary><a href="account.html"><i class="ti ti-key"></i>Account</a><a href="help.html"><i class="ti ti-help"></i>Help</a><form method="post" action="login.html"><button type="submit"><i class="ti ti-logout"></i>Sign out</button></form></details></div></details></div>
|
||||||
<div class="navbar-menu is-active">
|
<div class="navbar-menu">
|
||||||
<div class="navbar-start">
|
<div class="navbar-start">
|
||||||
<a class="navbar-item" href="overview.html">Overview</a>
|
<a class="navbar-item" href="overview.html">Overview</a>
|
||||||
<div class="navbar-item has-dropdown is-hoverable sp-current">
|
<div class="navbar-item has-dropdown is-hoverable sp-current">
|
||||||
@@ -69,14 +69,14 @@
|
|||||||
<div class="sp-grow">
|
<div class="sp-grow">
|
||||||
<p class="sp-kicker">Outbound</p>
|
<p class="sp-kicker">Outbound</p>
|
||||||
<h1 class="title is-3">Outbound queue</h1>
|
<h1 class="title is-3">Outbound queue</h1>
|
||||||
<p class="sp-lead">Messages accepted from applications and still waiting for the receiving server. Refreshes every 10 s.</p>
|
<p class="sp-lead">Messages accepted from applications and still waiting for the receiving server. Refreshes on its own.</p>
|
||||||
</div>
|
</div>
|
||||||
</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><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="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</th><th>From</th><th>To</th><th>Last reply</th><th></th></tr></thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
<tr><td class="sp-mono">4XcB7k2Jm9z1</td><td class="sp-mono sp-muted sp-nowrap sp-small">09-21 13:58:07</td><td>alerts@<wbr>example.org</td><td>noc@<wbr>example.org</td><td class="sp-muted">451 4.7.1 Greylisted, try again in 5 minutes</td><td class="sp-actions"><a href="out-message.html">Details</a></td></tr>
|
<tr><td class="sp-mono">4XcB7k2Jm9z1</td><td class="sp-mono sp-muted sp-nowrap sp-small">09-21 13:58:07</td><td>alerts@<wbr>example.org</td><td>noc@<wbr>example.org</td><td class="sp-muted">451 4.7.1 Greylisted, try again in 5 minutes</td><td class="sp-actions"><a href="out-message.html">Details</a></td></tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
@@ -95,7 +95,7 @@
|
|||||||
</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 · Copyright © 2026 Mikhail Yenuchenko · <a href="#">License (AGPL-3.0)</a> · <a href="#">Source</a> · No warranty</footer>
|
||||||
</div>
|
</div>
|
||||||
</main>
|
</main>
|
||||||
</body>
|
</body>
|
||||||
|
|||||||
@@ -90,7 +90,8 @@
|
|||||||
"box[Milter sockets]:table",
|
"box[Milter sockets]:table",
|
||||||
"box[TLS certificate]:body",
|
"box[TLS certificate]:body",
|
||||||
"box[Notification mail]:table+body",
|
"box[Notification mail]:table+body",
|
||||||
"box[Configuration]:body"
|
"box[Configuration]:body",
|
||||||
|
"box[Test message]:body"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
@@ -107,7 +108,7 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"is-9": [
|
"is-9": [
|
||||||
"box[Overview]:help×3",
|
"box[Overview]:help×4",
|
||||||
"box[Outbound]:help×7",
|
"box[Outbound]:help×7",
|
||||||
"box[Inbound]:help×6",
|
"box[Inbound]:help×6",
|
||||||
"box[Server]:help×4",
|
"box[Server]:help×4",
|
||||||
@@ -385,26 +386,27 @@
|
|||||||
"box[Send a test e-mail]:body"
|
"box[Send a test e-mail]:body"
|
||||||
],
|
],
|
||||||
"settings": [
|
"settings": [
|
||||||
"head",
|
"head+actions",
|
||||||
{
|
{
|
||||||
"form": [
|
"columns": [
|
||||||
{
|
{
|
||||||
"columns": [
|
"is-6": [
|
||||||
{
|
{
|
||||||
"is-6": [
|
"form": [
|
||||||
"box[Log retention]:body",
|
"box[Log retention]:body",
|
||||||
"box[Sending rate limits]:body"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"is-6": [
|
|
||||||
"box[Notifications]:body",
|
"box[Notifications]:body",
|
||||||
"box[Notification mail]:record×2"
|
"submit_row"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"submit_row"
|
{
|
||||||
|
"is-6": [
|
||||||
|
"box[Notification mail]:record×2",
|
||||||
|
"box[Sending rate limits]:body",
|
||||||
|
"box[Time zone]:body"
|
||||||
|
]
|
||||||
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -12,8 +12,8 @@
|
|||||||
<body>
|
<body>
|
||||||
<nav class="navbar is-primary" aria-label="main navigation">
|
<nav class="navbar is-primary" aria-label="main navigation">
|
||||||
<div class="container">
|
<div class="container">
|
||||||
<div class="navbar-brand"><a class="navbar-item" href="overview.html"><img src="../../selfpost-wordmark.svg" alt="SelfPost"></a></div>
|
<div class="navbar-brand"><a class="navbar-item" href="overview.html"><img src="../../selfpost-wordmark.svg" alt="SelfPost"></a><details class="sp-touchnav"><summary>Menu</summary><div class="sp-touchnav-panel"><a href="overview.html" aria-current="page">Overview</a><details class="sp-touchnav-group"><summary>Outbound</summary><a href="out-domains.html"><i class="ti ti-world-upload"></i>Domains</a><a href="out-log.html"><i class="ti ti-list-details"></i>Log</a><a href="out-queue.html"><i class="ti ti-stack-2"></i>Queue</a><a href="dmarc.html"><i class="ti ti-report-analytics"></i>DMARC reports</a></details><details class="sp-touchnav-group"><summary>Inbound</summary><a href="in-domains.html"><i class="ti ti-world-download"></i>Domains</a><a href="in-log.html"><i class="ti ti-list-details"></i>Log</a><a href="in-queue.html"><i class="ti ti-stack-2"></i>Queue</a><a href="in-quarantine.html"><i class="ti ti-mail-pause"></i>Quarantine</a><a href="in-filter-lists.html"><i class="ti ti-filter"></i>Filter lists</a></details><details class="sp-touchnav-group"><summary>Server</summary><a href="health.html"><i class="ti ti-heartbeat"></i>Health</a><a href="preflight.html"><i class="ti ti-checklist"></i>Preflight</a><a href="system-log.html"><i class="ti ti-file-text"></i>System log</a><a href="backup.html"><i class="ti ti-archive"></i>Backup</a><a href="users.html"><i class="ti ti-users"></i>Users</a><a href="settings.html"><i class="ti ti-settings"></i>Settings</a></details><details class="sp-touchnav-group"><summary><i class="ti ti-user-circle"></i>admin</summary><a href="account.html"><i class="ti ti-key"></i>Account</a><a href="help.html"><i class="ti ti-help"></i>Help</a><form method="post" action="login.html"><button type="submit"><i class="ti ti-logout"></i>Sign out</button></form></details></div></details></div>
|
||||||
<div class="navbar-menu is-active">
|
<div class="navbar-menu">
|
||||||
<div class="navbar-start">
|
<div class="navbar-start">
|
||||||
<a class="navbar-item sp-current" href="overview.html">Overview</a>
|
<a class="navbar-item sp-current" href="overview.html">Overview</a>
|
||||||
<div class="navbar-item has-dropdown is-hoverable">
|
<div class="navbar-item has-dropdown is-hoverable">
|
||||||
@@ -70,9 +70,9 @@
|
|||||||
<div class="sp-grow">
|
<div class="sp-grow">
|
||||||
<p class="sp-kicker">mail.example.org</p>
|
<p class="sp-kicker">mail.example.org</p>
|
||||||
<h1 class="title is-3">Mail is flowing. One thing needs attention.</h1>
|
<h1 class="title is-3">Mail is flowing. One thing needs attention.</h1>
|
||||||
<p class="sp-lead">The TLS certificate expires in 12 days and has not renewed yet.</p>
|
<p class="sp-lead"><a href="health.html">TLS certificate</a>: expires in 12 days.</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="sp-muted sp-small">Updated 14:02:31 UTC<br>refreshes every 10 s</div>
|
<div class="sp-muted sp-small">Updated 14:02:31<br>refreshes on its own</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="box">
|
<div class="box">
|
||||||
@@ -82,7 +82,7 @@
|
|||||||
<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"><span class="sp-h-icon"><i class="ti ti-server-cog"></i></span><span><span class="sp-h-name">Processes</span><span class="sp-h-value">5 of 5 running</span><span class="sp-h-sub">up 18 days</span></span></a>
|
||||||
<a href="health.html" class="sp-warn"><span class="sp-h-icon"><i class="ti ti-certificate"></i></span><span><span class="sp-h-name">TLS certificate</span><span class="sp-h-value">Expires in 12 days</span><span class="sp-h-sub">renewal has not run</span></span></a>
|
<a href="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"><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">5 of 5 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">6 of 6 answering</span><span class="sp-h-sub">incl. the spam filter</span></span></a>
|
||||||
<a href="health.html"><span class="sp-h-icon"><i class="ti ti-arrows-exchange"></i></span><span><span class="sp-h-name">Reverse DNS</span><span class="sp-h-value">Forward = reverse</span><span class="sp-h-sub">203.0.113.25</span></span></a>
|
<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>
|
||||||
@@ -115,7 +115,7 @@
|
|||||||
</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 · Copyright © 2026 Mikhail Yenuchenko · <a href="#">License (AGPL-3.0)</a> · <a href="#">Source</a> · No warranty</footer>
|
||||||
</div>
|
</div>
|
||||||
</main>
|
</main>
|
||||||
</body>
|
</body>
|
||||||
|
|||||||
@@ -12,8 +12,8 @@
|
|||||||
<body>
|
<body>
|
||||||
<nav class="navbar is-primary" aria-label="main navigation">
|
<nav class="navbar is-primary" aria-label="main navigation">
|
||||||
<div class="container">
|
<div class="container">
|
||||||
<div class="navbar-brand"><a class="navbar-item" href="overview.html"><img src="../../selfpost-wordmark.svg" alt="SelfPost"></a></div>
|
<div class="navbar-brand"><a class="navbar-item" href="overview.html"><img src="../../selfpost-wordmark.svg" alt="SelfPost"></a><details class="sp-touchnav"><summary>Menu</summary><div class="sp-touchnav-panel"><a href="overview.html">Overview</a><details class="sp-touchnav-group"><summary>Outbound</summary><a href="out-domains.html"><i class="ti ti-world-upload"></i>Domains</a><a href="out-log.html"><i class="ti ti-list-details"></i>Log</a><a href="out-queue.html"><i class="ti ti-stack-2"></i>Queue</a><a href="dmarc.html"><i class="ti ti-report-analytics"></i>DMARC reports</a></details><details class="sp-touchnav-group"><summary>Inbound</summary><a href="in-domains.html"><i class="ti ti-world-download"></i>Domains</a><a href="in-log.html"><i class="ti ti-list-details"></i>Log</a><a href="in-queue.html"><i class="ti ti-stack-2"></i>Queue</a><a href="in-quarantine.html"><i class="ti ti-mail-pause"></i>Quarantine</a><a href="in-filter-lists.html"><i class="ti ti-filter"></i>Filter lists</a></details><details class="sp-touchnav-group" open><summary>Server</summary><a href="health.html"><i class="ti ti-heartbeat"></i>Health</a><a href="preflight.html" aria-current="page"><i class="ti ti-checklist"></i>Preflight</a><a href="system-log.html"><i class="ti ti-file-text"></i>System log</a><a href="backup.html"><i class="ti ti-archive"></i>Backup</a><a href="users.html"><i class="ti ti-users"></i>Users</a><a href="settings.html"><i class="ti ti-settings"></i>Settings</a></details><details class="sp-touchnav-group"><summary><i class="ti ti-user-circle"></i>admin</summary><a href="account.html"><i class="ti ti-key"></i>Account</a><a href="help.html"><i class="ti ti-help"></i>Help</a><form method="post" action="login.html"><button type="submit"><i class="ti ti-logout"></i>Sign out</button></form></details></div></details></div>
|
||||||
<div class="navbar-menu is-active">
|
<div class="navbar-menu">
|
||||||
<div class="navbar-start">
|
<div class="navbar-start">
|
||||||
<a class="navbar-item" href="overview.html">Overview</a>
|
<a class="navbar-item" href="overview.html">Overview</a>
|
||||||
<div class="navbar-item has-dropdown is-hoverable">
|
<div class="navbar-item has-dropdown is-hoverable">
|
||||||
@@ -76,7 +76,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="box">
|
<div class="box">
|
||||||
<div class="sp-box-head"><span class="sp-no">01</span><h2>Checks</h2><div class="sp-end">Ran 14:02 UTC · 3.1 s</div></div>
|
<div class="sp-box-head"><span class="sp-no">01</span><h2>Checks</h2><div class="sp-end">Ran 14:02 · 3.1 s</div></div>
|
||||||
<div class="table-container"><table class="table is-fullwidth">
|
<div class="table-container"><table class="table is-fullwidth">
|
||||||
<thead><tr><th>Check</th><th>Result</th><th>Found</th><th>What to do</th></tr></thead>
|
<thead><tr><th>Check</th><th>Result</th><th>Found</th><th>What to do</th></tr></thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
@@ -108,7 +108,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="column is-6">
|
<div class="column is-6">
|
||||||
<dl class="sp-facts">
|
<dl class="sp-facts">
|
||||||
<div><dt>Last test</dt><dd>14:03 UTC to <span class="sp-mono">mix@elsewhere.example</span></dd></div>
|
<div><dt>Last test</dt><dd>14:03 to <span class="sp-mono">mix@elsewhere.example</span></dd></div>
|
||||||
<div><dt>Result</dt><dd><span class="tag is-success is-light">queued</span> <span class="sp-mono sp-small">4XcC1d8Pq2z1</span></dd></div>
|
<div><dt>Result</dt><dd><span class="tag is-success is-light">queued</span> <span class="sp-mono sp-small">4XcC1d8Pq2z1</span></dd></div>
|
||||||
<div><dt>Then</dt><dd><a href="out-message.html">Follow it in the outbound log</a></dd></div>
|
<div><dt>Then</dt><dd><a href="out-message.html">Follow it in the outbound log</a></dd></div>
|
||||||
</dl>
|
</dl>
|
||||||
@@ -117,7 +117,7 @@
|
|||||||
</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 · Copyright © 2026 Mikhail Yenuchenko · <a href="#">License (AGPL-3.0)</a> · <a href="#">Source</a> · No warranty</footer>
|
||||||
</div>
|
</div>
|
||||||
</main>
|
</main>
|
||||||
</body>
|
</body>
|
||||||
|
|||||||
@@ -12,8 +12,8 @@
|
|||||||
<body>
|
<body>
|
||||||
<nav class="navbar is-primary" aria-label="main navigation">
|
<nav class="navbar is-primary" aria-label="main navigation">
|
||||||
<div class="container">
|
<div class="container">
|
||||||
<div class="navbar-brand"><a class="navbar-item" href="overview.html"><img src="../../selfpost-wordmark.svg" alt="SelfPost"></a></div>
|
<div class="navbar-brand"><a class="navbar-item" href="overview.html"><img src="../../selfpost-wordmark.svg" alt="SelfPost"></a><details class="sp-touchnav"><summary>Menu</summary><div class="sp-touchnav-panel"><a href="overview.html">Overview</a><details class="sp-touchnav-group"><summary>Outbound</summary><a href="out-domains.html"><i class="ti ti-world-upload"></i>Domains</a><a href="out-log.html"><i class="ti ti-list-details"></i>Log</a><a href="out-queue.html"><i class="ti ti-stack-2"></i>Queue</a><a href="dmarc.html"><i class="ti ti-report-analytics"></i>DMARC reports</a></details><details class="sp-touchnav-group"><summary>Inbound</summary><a href="in-domains.html"><i class="ti ti-world-download"></i>Domains</a><a href="in-log.html"><i class="ti ti-list-details"></i>Log</a><a href="in-queue.html"><i class="ti ti-stack-2"></i>Queue</a><a href="in-quarantine.html"><i class="ti ti-mail-pause"></i>Quarantine</a><a href="in-filter-lists.html"><i class="ti ti-filter"></i>Filter lists</a></details><details class="sp-touchnav-group" open><summary>Server</summary><a href="health.html"><i class="ti ti-heartbeat"></i>Health</a><a href="preflight.html"><i class="ti ti-checklist"></i>Preflight</a><a href="system-log.html"><i class="ti ti-file-text"></i>System log</a><a href="backup.html"><i class="ti ti-archive"></i>Backup</a><a href="users.html"><i class="ti ti-users"></i>Users</a><a href="settings.html" aria-current="page"><i class="ti ti-settings"></i>Settings</a></details><details class="sp-touchnav-group"><summary><i class="ti ti-user-circle"></i>admin</summary><a href="account.html"><i class="ti ti-key"></i>Account</a><a href="help.html"><i class="ti ti-help"></i>Help</a><form method="post" action="login.html"><button type="submit"><i class="ti ti-logout"></i>Sign out</button></form></details></div></details></div>
|
||||||
<div class="navbar-menu is-active">
|
<div class="navbar-menu">
|
||||||
<div class="navbar-start">
|
<div class="navbar-start">
|
||||||
<a class="navbar-item" href="overview.html">Overview</a>
|
<a class="navbar-item" href="overview.html">Overview</a>
|
||||||
<div class="navbar-item has-dropdown is-hoverable">
|
<div class="navbar-item has-dropdown is-hoverable">
|
||||||
@@ -71,11 +71,14 @@
|
|||||||
<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 and password 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 class="buttons">
|
||||||
|
<form method="post" action="#"><button type="submit" class="button"><i class="ti ti-refresh"></i>Re-check DNS</button></form>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<form class="sp-form" method="post" action="#">
|
<div class="columns">
|
||||||
<div class="columns">
|
<div class="column is-6">
|
||||||
<div class="column is-6">
|
<form class="sp-form" method="post" action="#">
|
||||||
<div class="box">
|
<div class="box">
|
||||||
<div class="sp-box-head"><span class="sp-no">01</span><h2>Log retention</h2><a class="sp-help" href="help.html#settings" title="How long rows are kept"><i class="ti ti-help-circle"></i></a></div>
|
<div class="sp-box-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">
|
||||||
@@ -88,58 +91,68 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="box" id="rate-limits">
|
|
||||||
<div class="sp-box-head"><span class="sp-no">02</span><h2>Sending rate limits</h2><div class="sp-end">Read-only</div><a class="sp-help" href="help.html#limits" title="Levels 1 and 2"><i class="ti ti-help-circle"></i></a></div>
|
|
||||||
<div class="sp-box-body">
|
|
||||||
<dl class="sp-facts">
|
|
||||||
<div><dt>Level 1 · per client IP</dt><dd class="sp-big">600 / h</dd><dd class="sp-muted">Set in <code>.env</code>; restart to change. Nothing below may exceed it.</dd></div>
|
|
||||||
<div><dt>Level 2 · domain, application</dt><dd>Set on each <a href="out-domain-settings.html">domain</a> and <a href="out-app.html">application</a>.</dd></div>
|
|
||||||
</dl>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="column is-6">
|
|
||||||
<div class="box" id="notifications">
|
<div class="box" id="notifications">
|
||||||
<div class="sp-box-head"><span class="sp-no">03</span><h2>Notifications</h2><a class="sp-help" href="help.html#notifications" title="What is sent and when"><i class="ti ti-help-circle"></i></a></div>
|
<div class="sp-box-head"><span class="sp-no">02</span><h2>Notifications</h2><a class="sp-help" href="help.html#notifications" title="What is sent and when"><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="notify" checked> Send notification e-mails</label>
|
<div class="field"><label class="checkbox"><input type="checkbox" name="notify" checked> Send notification e-mails</label>
|
||||||
<p class="help">From <span class="sp-mono">postmaster@mail.example.org</span>, to every user who has an e-mail and has not turned them off under <a href="account.html">Account</a>.</p></div>
|
<p class="help">From <span class="sp-mono">postmaster@mail.example.org</span>, to every user who has an e-mail and has not turned them off under <a href="account.html">Account</a>.</p></div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="buttons"><button type="submit" class="button is-primary">Save settings</button></div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="box" id="notification-mail">
|
<div class="column is-6">
|
||||||
<div class="sp-box-head"><span class="sp-no">04</span><h2>Notification mail</h2><div class="sp-end">DNS records of the server's own name</div><a class="sp-help" href="help.html#notifications" title="What the panel sends and what makes it arrive"><i class="ti ti-help-circle"></i></a></div>
|
<div class="box" id="notification-mail">
|
||||||
|
<div class="sp-box-head"><span class="sp-no">03</span><h2>Notification mail</h2><div class="sp-end">The server's own name · checked 3 min ago</div><a class="sp-help" href="help.html#notifications" title="What the panel sends and what makes it arrive"><i class="ti ti-help-circle"></i></a></div>
|
||||||
|
|
||||||
<div class="sp-record">
|
<details class="sp-record">
|
||||||
<div class="sp-record-head"><h3>DKIM</h3><span class="tag is-success is-light">published</span><small>Not a secret · selector <code>sp2026</code></small></div>
|
<summary class="sp-record-head"><h3>DKIM</h3><span class="tag is-success is-light">published</span><small>Not a secret · selector <code>sp2026</code></small><span class="sp-record-toggle"><i class="ti ti-eye"></i><span>Show values</span><span>Hide values</span></span></summary>
|
||||||
<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="sp2026._domainkey.mail.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="sp2026._domainkey.mail.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 class="field"><label class="label">Value to publish</label>
|
|
||||||
<div class="field has-addons"><div class="control is-expanded"><textarea class="textarea" rows="5" readonly>v=DKIM1; k=rsa; p=MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAq7n2Lw0c5vXeR1dYt8uJb3mKs6pHf9aZoWg4iNy2TqCx0lVjE7rUh5sBk1dMa8fPz3oGc6yXw9tIe4nQv2uL…IDAQAB</textarea></div><div class="control"><button type="button" class="button"><i class="ti ti-copy"></i>Copy</button></div></div></div>
|
|
||||||
</div>
|
</div>
|
||||||
|
<div class="field"><label class="label">Value to publish</label>
|
||||||
|
<div class="field has-addons"><div class="control is-expanded"><textarea class="textarea" rows="5" readonly>v=DKIM1; k=rsa; p=MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAq7n2Lw0c5vXeR1dYt8uJb3mKs6pHf9aZoWg4iNy2TqCx0lVjE7rUh5sBk1dMa8fPz3oGc6yXw9tIe4nQv2uL…IDAQAB</textarea></div><div class="control"><button type="button" class="button"><i class="ti ti-copy"></i>Copy</button></div></div></div>
|
||||||
|
</details>
|
||||||
|
|
||||||
<div class="sp-record">
|
<div class="sp-record">
|
||||||
<div class="sp-record-head"><h3>SPF</h3><span class="tag is-warning is-light">missing</span><small>For the server's own name, not for your domains</small></div>
|
<div class="sp-record-head"><h3>SPF</h3><span class="tag is-warning is-light">missing</span><small>For the server's own name, not for your domains</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="mail.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="mail.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 class="field"><label class="label">Value to publish</label>
|
|
||||||
<div class="field has-addons"><div class="control is-expanded"><input class="input" readonly value="v=spf1 a -all"></div><div class="control"><button type="button" class="button"><i class="ti ti-copy"></i>Copy</button></div></div></div>
|
|
||||||
<p class="help">Notifications are sent without it, and a receiver may file them as spam.</p>
|
|
||||||
</div>
|
</div>
|
||||||
|
<div class="field"><label class="label">Value to publish</label>
|
||||||
|
<div class="field has-addons"><div class="control is-expanded"><input class="input" readonly value="v=spf1 a -all"></div><div class="control"><button type="button" class="button"><i class="ti ti-copy"></i>Copy</button></div></div></div>
|
||||||
|
<p class="help">Notifications are sent without it, and a receiver may file them as spam.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="box" id="rate-limits">
|
||||||
|
<div class="sp-box-head"><span class="sp-no">04</span><h2>Sending rate limits</h2><div class="sp-end">Read-only</div><a class="sp-help" href="help.html#limits" title="Levels 1 and 2"><i class="ti ti-help-circle"></i></a></div>
|
||||||
|
<div class="sp-box-body">
|
||||||
|
<dl class="sp-facts">
|
||||||
|
<div><dt>Level 1 · per client IP</dt><dd class="sp-big">600 / h</dd><dd class="sp-muted">Set in <code>.env</code>; restart to change. Nothing below may exceed it.</dd></div>
|
||||||
|
<div><dt>Level 2 · domain, application</dt><dd>Set on each <a href="out-domain-settings.html">domain</a> and <a href="out-app.html">application</a>.</dd></div>
|
||||||
|
</dl>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="box" id="time-zone">
|
||||||
|
<div class="sp-box-head"><span class="sp-no">05</span><h2>Time zone</h2><div class="sp-end">Read-only</div><a class="sp-help" href="help.html#settings" title="Times and days of the panel"><i class="ti ti-help-circle"></i></a></div>
|
||||||
|
<div class="sp-box-body">
|
||||||
|
<dl class="sp-facts">
|
||||||
|
<div><dt>Zone</dt><dd class="sp-mono">Europe/Moscow</dd><dd class="sp-muted">UTC+03:00. Set in <code>.env</code> (<code>TZ</code>); restart to change.</dd></div>
|
||||||
|
</dl>
|
||||||
|
<p class="mt-4 sp-muted">Every time the panel shows, and every day it counts - today on Health, the days of the statistics - is this zone's, and so are the stamps of <code>mail.log</code>. One exception: the day a DMARC report covers is the reporter's, in UTC.</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="buttons"><button type="submit" class="button is-primary">Save settings</button></div>
|
</div>
|
||||||
</form>
|
|
||||||
|
|
||||||
<footer class="sp-foot">SelfPost 1.8.0 · © 2026 Mix · <a href="#">License (AGPL-3.0)</a> · <a href="#">Source</a> · No warranty</footer>
|
<footer class="sp-foot">SelfPost 1.8.0 · Copyright © 2026 Mikhail Yenuchenko · <a href="#">License (AGPL-3.0)</a> · <a href="#">Source</a> · No warranty</footer>
|
||||||
</div>
|
</div>
|
||||||
</main>
|
</main>
|
||||||
</body>
|
</body>
|
||||||
|
|||||||
@@ -36,7 +36,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="field mt-5"><button type="submit" class="button is-primary is-fullwidth">Create administrator</button></div>
|
<div class="field mt-5"><button type="submit" class="button is-primary is-fullwidth">Create administrator</button></div>
|
||||||
</form>
|
</form>
|
||||||
<p class="sp-foot">© 2026 Mix · <a href="#">License (AGPL-3.0)</a> · <a href="#">Source</a> · No warranty</p>
|
<p class="sp-foot">Copyright © 2026 Mikhail Yenuchenko · <a href="#">License (AGPL-3.0)</a> · <a href="#">Source</a> · No warranty</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -14,12 +14,18 @@
|
|||||||
<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 <span class="tag is-warning is-light">not confirmed</span></label><div class="control has-icons-left"><input class="input" id="email" name="email" type="email" value="mix@example.org" autocomplete="email"><span class="icon is-left"><i class="ti ti-mail"></i></span></div>
|
||||||
<p class="help">Your own address. A domain's report address can be filled with it from Domain settings.</p></div>
|
<p class="help">Your own address. Saving a new one sends it a link to confirm it; once confirmed it is where a link to reset your password can go. A domain's report address can be filled with it from Domain settings.</p></div>
|
||||||
|
<div class="field"><label class="label" for="pcur">Current password</label><div class="control"><input class="input" id="pcur" name="current_password" type="password" autocomplete="current-password"></div>
|
||||||
|
<p class="help">Needed only to change the e-mail.</p></div>
|
||||||
<div class="field"><label class="checkbox"><input type="checkbox" name="notify" checked> Send me notifications</label>
|
<div class="field"><label class="checkbox"><input type="checkbox" name="notify" checked> Send me notifications</label>
|
||||||
<p class="help">To this address, when something needs you: a DNS record of a domain stops matching, a rate limit refuses mail, the TLS certificate is about to expire, a message has waited in the queue for three days. <a href="help.html#notifications">What is sent and when</a></p></div>
|
<p class="help">To this address, when something needs you: a DNS record of a domain stops matching, a rate limit refuses mail, the TLS certificate is about to expire, a message has waited in the queue for three days. <a href="help.html#notifications">What is sent and when</a></p></div>
|
||||||
<button type="submit" class="button is-primary">Save profile</button>
|
<button type="submit" class="button is-primary">Save profile</button>
|
||||||
</form>
|
</form>
|
||||||
|
<form class="sp-form mt-4" method="post" action="#">
|
||||||
|
<p class="help mb-3">A link was sent to <span class="sp-mono">mix@example.org</span> at 14:02; it works once, for 24 hours.</p>
|
||||||
|
<button type="submit" class="button">Send the link again</button>
|
||||||
|
</form>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -20,6 +20,8 @@
|
|||||||
<div class="field"><label class="label" for="pw2">Repeat password</label><div class="control"><input class="input" id="pw2" type="password"></div></div>
|
<div class="field"><label class="label" for="pw2">Repeat password</label><div class="control"><input class="input" id="pw2" type="password"></div></div>
|
||||||
</div>
|
</div>
|
||||||
<p class="help mb-3">Keep this password: SelfPost does not store it, and the file cannot be opened without it.</p>
|
<p class="help mb-3">Keep this password: SelfPost does not store it, and the file cannot be opened without it.</p>
|
||||||
|
<div class="field"><label class="label" for="acpw">Your account password</label><div class="control"><input class="input" id="acpw" name="account_password" type="password" autocomplete="current-password" required></div>
|
||||||
|
<p class="help">The one you sign in with, asked at every download: the archive holds every secret of the server, and a session left open must not be enough to take it.</p></div>
|
||||||
<button type="submit" class="button is-primary"><i class="ti ti-download"></i>Download full backup</button>
|
<button type="submit" class="button is-primary"><i class="ti ti-download"></i>Download full backup</button>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -84,9 +84,9 @@
|
|||||||
<div class="columns">
|
<div class="columns">
|
||||||
<div class="column is-7">
|
<div class="column is-7">
|
||||||
<div class="box">
|
<div class="box">
|
||||||
<div class="sp-box-head"><span class="sp-no">05</span><h2>dns_record · copy_field</h2><div class="sp-end">One block per record</div></div>
|
<div class="sp-box-head"><span class="sp-no">05</span><h2>dns_record · copy_field</h2><div class="sp-end">One block per record · folded when DNS holds it as shown</div></div>
|
||||||
<div class="sp-record">
|
<details class="sp-record">
|
||||||
<div class="sp-record-head"><h3>Record</h3><span class="tag is-success is-light">published</span><small>A one-line note</small></div>
|
<summary class="sp-record-head"><h3>Record</h3><span class="tag is-success is-light">published</span><small>A one-line note</small><span class="sp-record-toggle"><i class="ti ti-eye"></i><span>Show values</span><span>Hide values</span></span></summary>
|
||||||
<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="copy_field — single line"></div><div class="control"><button type="button" class="button"><i class="ti ti-copy"></i>Copy</button></div></div></div>
|
<div class="field has-addons"><div class="control is-expanded"><input class="input" readonly value="copy_field — single line"></div><div class="control"><button type="button" class="button"><i class="ti ti-copy"></i>Copy</button></div></div></div>
|
||||||
@@ -94,7 +94,7 @@
|
|||||||
</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"><textarea class="textarea" rows="2" readonly>copy_field — multi-line, for a DKIM key</textarea></div><div class="control"><button type="button" class="button"><i class="ti ti-copy"></i>Copy</button></div></div></div>
|
<div class="field has-addons"><div class="control is-expanded"><textarea class="textarea" rows="2" readonly>copy_field — multi-line, for a DKIM key</textarea></div><div class="control"><button type="button" class="button"><i class="ti ti-copy"></i>Copy</button></div></div></div>
|
||||||
</div>
|
</details>
|
||||||
<div class="sp-record">
|
<div class="sp-record">
|
||||||
<div class="sp-record-head"><h3>Record</h3><span class="tag is-danger is-light">mismatch</span><small>"In DNS now" appears only when it differs</small></div>
|
<div class="sp-record-head"><h3>Record</h3><span class="tag is-danger is-light">mismatch</span><small>"In DNS now" appears only when it differs</small></div>
|
||||||
<div class="field"><label class="label">In DNS now</label>
|
<div class="field"><label class="label">In DNS now</label>
|
||||||
@@ -106,7 +106,7 @@
|
|||||||
<div class="box">
|
<div class="box">
|
||||||
<div class="sp-box-head"><span class="sp-no">07</span><h2>Table conventions</h2><div class="sp-end">Always inside <code>table-container</code> · an address breaks after its @</div></div>
|
<div class="sp-box-head"><span class="sp-no">07</span><h2>Table conventions</h2><div class="sp-end">Always inside <code>table-container</code> · an address breaks after its @</div></div>
|
||||||
<div class="table-container"><table class="table is-fullwidth is-hoverable">
|
<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</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><form method="post" action="#"><button type="submit" class="has-text-danger">Delete</button></form></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>
|
||||||
@@ -116,9 +116,10 @@
|
|||||||
|
|
||||||
<div class="box">
|
<div class="box">
|
||||||
<div class="sp-box-head"><span class="sp-no">09</span><h2>log_pane</h2><div class="sp-end">sp-d time · sp-w warning · sp-e error</div></div>
|
<div class="sp-box-head"><span class="sp-no">09</span><h2>log_pane</h2><div class="sp-end">sp-d time · sp-w warning · sp-e error</div></div>
|
||||||
<pre class="sp-log"><span class="sp-d">13:58:07</span> an ordinary line
|
<pre class="sp-log"><span class="sp-ll"><span class="sp-d">13:58:07</span> an ordinary line
|
||||||
<span class="sp-d">13:58:09</span> <span class="sp-w">a deferred delivery — sp-w</span>
|
</span><span class="sp-ll"><span class="sp-d">13:58:09</span> <span class="sp-w">a deferred delivery — sp-w</span>
|
||||||
<span class="sp-d">13:57:41</span> <span class="sp-e">a reject or a bounce — sp-e</span></pre>
|
</span><span class="sp-ll"><span class="sp-d">13:57:41</span> <span class="sp-e">a reject or a bounce — sp-e</span>
|
||||||
|
</span></pre>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -129,7 +130,9 @@
|
|||||||
<dl class="sp-facts">
|
<dl class="sp-facts">
|
||||||
<div><dt>Label</dt><dd>Value</dd></div>
|
<div><dt>Label</dt><dd>Value</dd></div>
|
||||||
<div><dt>Mono value</dt><dd class="sp-mono">4XcB7k2Jm9z1</dd></div>
|
<div><dt>Mono value</dt><dd class="sp-mono">4XcB7k2Jm9z1</dd></div>
|
||||||
|
<div><dt>Label with a tag <span class="tag is-success is-light">matches</span></dt><dd>a verdict on the value</dd></div>
|
||||||
<div><dt>sp-big</dt><dd class="sp-big">5 days</dd><dd class="sp-muted">a supporting line</dd></div>
|
<div><dt>sp-big</dt><dd class="sp-big">5 days</dd><dd class="sp-muted">a supporting line</dd></div>
|
||||||
|
<div class="sp-wide"><dt>sp-wide</dt><dd class="sp-mono">a value that takes the whole row, a line of a server's own</dd></div>
|
||||||
</dl>
|
</dl>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -138,9 +141,9 @@
|
|||||||
<div class="sp-box-head"><span class="sp-no">08</span><h2>timeline</h2></div>
|
<div class="sp-box-head"><span class="sp-no">08</span><h2>timeline</h2></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>Done</strong> — default</li>
|
<li><time>13:58:07</time><strong>Done</strong> — default</li>
|
||||||
<li class="sp-warn"><time>13:58:09 UTC</time><strong>Deferred</strong> — sp-warn</li>
|
<li class="sp-warn"><time>13:58:09</time><strong>Deferred</strong> — sp-warn</li>
|
||||||
<li class="sp-fail"><time>14:40:00 UTC</time><strong>Bounced</strong> — sp-fail</li>
|
<li class="sp-fail"><time>14:40:00</time><strong>Bounced</strong> — sp-fail</li>
|
||||||
<li class="sp-pending"><time>not yet</time>Expected next — sp-pending</li>
|
<li class="sp-pending"><time>not yet</time>Expected next — sp-pending</li>
|
||||||
</ol>
|
</ol>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -0,0 +1,18 @@
|
|||||||
|
<!-- title: Confirm e-mail | 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">Confirm e-mail</h1>
|
||||||
|
<div class="notification is-danger is-light"><i class="ti ti-alert-circle"></i>Sign in to the account in this browser, then open the link again.</div>
|
||||||
|
<p><a href="login.html">Sign in</a></p>
|
||||||
|
</div>
|
||||||
|
<p class="sp-foot">Copyright © 2026 Mikhail Yenuchenko · <a href="#">License (AGPL-3.0)</a> · <a href="#">Source</a> · No warranty</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
<!-- title: Confirm e-mail | 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">
|
||||||
|
<form class="sp-form" method="post" action="account.html">
|
||||||
|
<p class="sp-kicker">mail.example.org · admin</p>
|
||||||
|
<h1 class="title is-3">Confirm e-mail</h1>
|
||||||
|
<p class="sp-muted mb-4"><span class="sp-mono">mix@example.org</span> becomes the confirmed address of this account: where a link to reset its password can be sent.</p>
|
||||||
|
<div class="field mt-5"><button type="submit" class="button is-primary is-fullwidth">Confirm</button></div>
|
||||||
|
</form>
|
||||||
|
<p class="sp-foot">Copyright © 2026 Mikhail Yenuchenko · <a href="#">License (AGPL-3.0)</a> · <a href="#">Source</a> · No warranty</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
@@ -26,7 +26,7 @@
|
|||||||
<div class="box">
|
<div class="box">
|
||||||
<div class="sp-box-head"><span class="sp-no">02</span><h2>Reports</h2></div>
|
<div class="sp-box-head"><span class="sp-no">02</span><h2>Reports</h2></div>
|
||||||
<div class="table-container"><table class="table is-fullwidth is-hoverable">
|
<div class="table-container"><table class="table is-fullwidth is-hoverable">
|
||||||
<thead><tr><th>Received, UTC</th><th>Reporter</th><th>Window</th><th>Pass</th><th>Fail</th><th></th></tr></thead>
|
<thead><tr><th>Received</th><th>Reporter</th><th>Window</th><th>Pass</th><th>Fail</th><th></th></tr></thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
<tr><td class="sp-mono sp-muted sp-nowrap sp-small">09-21 04:40</td><td class="sp-muted">Outlook.com</td><td>20 Sep</td><td>1 903</td><td><span class="tag is-warning is-light">7</span></td><td class="sp-actions"><a href="dmarc-report.html">View</a></td></tr>
|
<tr><td class="sp-mono sp-muted sp-nowrap sp-small">09-21 04:40</td><td class="sp-muted">Outlook.com</td><td>20 Sep</td><td>1 903</td><td><span class="tag is-warning is-light">7</span></td><td class="sp-actions"><a href="dmarc-report.html">View</a></td></tr>
|
||||||
<tr><td class="sp-mono sp-muted sp-nowrap sp-small">09-21 02:15</td><td class="sp-muted">google.com</td><td>20 Sep</td><td>6 410</td><td>0</td><td class="sp-actions"><a href="dmarc-report.html">View</a></td></tr>
|
<tr><td class="sp-mono sp-muted sp-nowrap sp-small">09-21 02:15</td><td class="sp-muted">google.com</td><td>20 Sep</td><td>6 410</td><td>0</td><td class="sp-actions"><a href="dmarc-report.html">View</a></td></tr>
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
<div class="sp-head">
|
<div class="sp-head">
|
||||||
<div class="sp-grow">
|
<div class="sp-grow">
|
||||||
<p class="sp-kicker"><a href="dmarc.html">DMARC reports</a> / <a href="dmarc-domain.html">shop.example.org</a> /</p>
|
<p class="sp-kicker"><a href="dmarc.html">DMARC reports</a> / <a href="dmarc-domain.html">shop.example.org</a> /</p>
|
||||||
<h1 class="title is-4">Outlook.com · 20 Sep 2026</h1>
|
<h1 class="title is-4">Outlook.com · 20 Sep</h1>
|
||||||
<p class="sp-route"><span>shop.example.org</span><span class="tag is-success is-light">1 903 pass</span><span class="tag is-warning is-light">7 fail</span></p>
|
<p class="sp-route"><span>shop.example.org</span><span class="tag is-success is-light">1 903 pass</span><span class="tag is-warning is-light">7 fail</span></p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -15,8 +15,8 @@
|
|||||||
<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>
|
||||||
<div><dt>Report id</dt><dd class="sp-mono sp-small">5f1c9a0e7b2d4e61a3</dd></div>
|
<div><dt>Report id</dt><dd class="sp-mono sp-small">5f1c9a0e7b2d4e61a3</dd></div>
|
||||||
<div><dt>Window</dt><dd>20 Sep 00:00 – 21 Sep 00:00 UTC</dd></div>
|
<div><dt>Window</dt><dd>20 Sep 03:00 – 21 Sep 03:00</dd><dd class="sp-muted">The reporter's day, 20 Sep: reports count their days in UTC, and the title and the lists name that day. The times are this panel's zone.</dd></div>
|
||||||
<div><dt>Received</dt><dd>2026-09-21 04:40 UTC</dd></div>
|
<div><dt>Received</dt><dd>2026-09-21 04:40</dd></div>
|
||||||
</dl>
|
</dl>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -11,7 +11,7 @@
|
|||||||
<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-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</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 addresses</dt><dd class="sp-mono sp-small">dmarc-reports+<domain>@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 a domain or 90 days</dd></div>
|
<div><dt>Retention</dt><dd>500 reports a domain or 90 days</dd></div>
|
||||||
@@ -22,7 +22,7 @@
|
|||||||
<div class="box">
|
<div class="box">
|
||||||
<div class="sp-box-head"><span class="sp-no">02</span><h2>Recent reports</h2></div>
|
<div class="sp-box-head"><span class="sp-no">02</span><h2>Recent reports</h2></div>
|
||||||
<div class="table-container"><table class="table is-fullwidth is-hoverable">
|
<div class="table-container"><table class="table is-fullwidth is-hoverable">
|
||||||
<thead><tr><th>Received, UTC</th><th>Domain</th><th>Reporter</th><th>Window</th><th>Pass</th><th>Fail</th><th></th></tr></thead>
|
<thead><tr><th>Received</th><th>Domain</th><th>Reporter</th><th>Window</th><th>Pass</th><th>Fail</th><th></th></tr></thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
<tr><td class="sp-mono sp-muted sp-nowrap sp-small">09-21 06:12</td><td><a href="dmarc-domain.html">example.org</a></td><td class="sp-muted">google.com</td><td>20 Sep</td><td>412</td><td>0</td><td class="sp-actions"><a href="dmarc-report.html">View</a></td></tr>
|
<tr><td class="sp-mono sp-muted sp-nowrap sp-small">09-21 06:12</td><td><a href="dmarc-domain.html">example.org</a></td><td class="sp-muted">google.com</td><td>20 Sep</td><td>412</td><td>0</td><td class="sp-actions"><a href="dmarc-report.html">View</a></td></tr>
|
||||||
<tr><td class="sp-mono sp-muted sp-nowrap sp-small">09-21 04:40</td><td><a href="dmarc-domain.html">shop.example.org</a></td><td class="sp-muted">Outlook.com</td><td>20 Sep</td><td>1 903</td><td><span class="tag is-warning is-light">7</span></td><td class="sp-actions"><a href="dmarc-report.html">View</a></td></tr>
|
<tr><td class="sp-mono sp-muted sp-nowrap sp-small">09-21 04:40</td><td><a href="dmarc-domain.html">shop.example.org</a></td><td class="sp-muted">Outlook.com</td><td>20 Sep</td><td>1 903</td><td><span class="tag is-warning is-light">7</span></td><td class="sp-actions"><a href="dmarc-report.html">View</a></td></tr>
|
||||||
|
|||||||
@@ -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">Health</h1>
|
<h1 class="title is-3">Health</h1>
|
||||||
<p class="sp-lead">Everything behind the six cards on Overview. Refreshes every 10 s.</p>
|
<p class="sp-lead">Everything behind the six cards on Overview. Refreshes automatically.</p>
|
||||||
</div>
|
</div>
|
||||||
<form method="post" action="#"><button type="submit" class="button"><i class="ti ti-refresh"></i>Re-check DNS</button></form>
|
<form method="post" action="#"><button type="submit" class="button"><i class="ti ti-refresh"></i>Re-check DNS</button></form>
|
||||||
</div>
|
</div>
|
||||||
@@ -42,6 +42,7 @@
|
|||||||
<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>
|
||||||
<div><dt>Lookup</dt><dd class="sp-mono">→ 203.0.113.25<br>← mail.example.org</dd></div>
|
<div><dt>Lookup</dt><dd class="sp-mono">→ 203.0.113.25<br>← mail.example.org</dd></div>
|
||||||
|
<div class="sp-wide"><dt>Greeting</dt><dd class="sp-mono">220 mail.example.org ESMTP</dd></div>
|
||||||
</dl>
|
</dl>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -67,6 +68,7 @@
|
|||||||
<tr><td>dkim-signer <span class="sp-muted">· notifications</span><br><span class="sp-muted sp-mono sp-small">unix:/run/selfpost/sign-local.sock</span></td><td><span class="tag is-success is-light">ok</span></td></tr>
|
<tr><td>dkim-signer <span class="sp-muted">· notifications</span><br><span class="sp-muted sp-mono sp-small">unix:/run/selfpost/sign-local.sock</span></td><td><span class="tag is-success is-light">ok</span></td></tr>
|
||||||
<tr><td>journal-milter<br><span class="sp-muted sp-mono sp-small">unix:/run/selfpost/journal.sock</span></td><td><span class="tag is-success is-light">ok</span></td></tr>
|
<tr><td>journal-milter<br><span class="sp-muted sp-mono sp-small">unix:/run/selfpost/journal.sock</span></td><td><span class="tag is-success is-light">ok</span></td></tr>
|
||||||
<tr><td>sender-check<br><span class="sp-muted sp-mono sp-small">unix:/run/selfpost/sender.sock</span></td><td><span class="tag is-success is-light">ok</span></td></tr>
|
<tr><td>sender-check<br><span class="sp-muted sp-mono sp-small">unix:/run/selfpost/sender.sock</span></td><td><span class="tag is-success is-light">ok</span></td></tr>
|
||||||
|
<tr><td>inbound-journal-milter <span class="sp-muted">· Inbound log</span><br><span class="sp-muted sp-mono sp-small">unix:/run/selfpost/inbound-journal.sock</span></td><td><span class="tag is-success is-light">ok</span></td></tr>
|
||||||
<tr><td>rspamd <span class="sp-muted">· inbound spam filter</span><br><span class="sp-muted sp-mono sp-small">inet:antispam:11332</span></td><td><span class="tag is-success is-light">ok</span></td></tr>
|
<tr><td>rspamd <span class="sp-muted">· inbound spam filter</span><br><span class="sp-muted sp-mono sp-small">inet:antispam:11332</span></td><td><span class="tag is-success is-light">ok</span></td></tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
</table></div>
|
</table></div>
|
||||||
@@ -75,10 +77,10 @@
|
|||||||
<div class="sp-box-head"><span class="sp-no">04</span><h2>TLS certificate</h2><div class="sp-end"><span class="tag is-success is-light">ok</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-head"><span class="sp-no">04</span><h2>TLS certificate</h2><div class="sp-end"><span class="tag is-success is-light">ok</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-11-02 09:14 UTC</dd></div>
|
<div><dt>Expires</dt><dd class="sp-mono">2026-11-02 09:14</dd></div>
|
||||||
<div><dt>Names</dt><dd class="sp-mono">mail.example.org</dd></div>
|
<div><dt>Names <span class="tag is-success is-light">matches</span></dt><dd class="sp-mono">mail.example.org</dd></div>
|
||||||
<div><dt>Renewal expires</dt><dd class="sp-mono">2027-01-31 08:02 UTC</dd></div>
|
<div><dt>Renewal expires</dt><dd class="sp-mono">2027-01-31 08:02</dd></div>
|
||||||
<div><dt>Renewal mounted</dt><dd class="sp-mono">2026-10-15 03:12 UTC</dd></div>
|
<div><dt>Renewal mounted</dt><dd class="sp-mono">2026-10-15 03:12</dd></div>
|
||||||
</dl>
|
</dl>
|
||||||
<p class="mt-4 sp-muted">Postfix still serves the certificate above. A renewed one is waiting in the mounted file; Postfix takes it up at its next check for a renewal, at most a day after it was mounted.</p>
|
<p class="mt-4 sp-muted">Postfix still serves the certificate above. A renewed one is waiting in the mounted file; Postfix takes it up at its next check for a renewal, at most a day after it was mounted.</p>
|
||||||
</div>
|
</div>
|
||||||
@@ -99,9 +101,25 @@
|
|||||||
<div class="box" id="configuration" data-order="8">
|
<div class="box" id="configuration" data-order="8">
|
||||||
<div class="sp-box-head"><span class="sp-no">08</span><h2>Configuration</h2><div class="sp-end">Safe to run at any time</div></div>
|
<div class="sp-box-head"><span class="sp-no">08</span><h2>Configuration</h2><div class="sp-end">Safe to run at any time</div></div>
|
||||||
<div class="sp-box-body">
|
<div class="sp-box-body">
|
||||||
|
<div class="mb-3"><dl class="sp-facts">
|
||||||
|
<div><dt>Your address</dt><dd class="sp-mono">198.51.100.7</dd><dd class="sp-muted">From X-Forwarded-For, which the trusted proxy 172.18.0.1 set</dd></div>
|
||||||
|
</dl></div>
|
||||||
<p class="mb-3">Regenerates the Postfix configuration and the spam filter's lists from the database, removes the keys of deleted domains, and reloads Postfix. Use it after editing the files by hand, after restoring a backup, or when the running configuration looks out of step with the domain and application lists. It does not touch the queue or the TLS certificate.</p>
|
<p class="mb-3">Regenerates the Postfix configuration and the spam filter's lists from the database, removes the keys of deleted domains, and reloads Postfix. Use it after editing the files by hand, after restoring a backup, or when the running configuration looks out of step with the domain and application lists. It does not touch the queue or the TLS certificate.</p>
|
||||||
<form method="post" action="#"><button type="submit" class="button"><i class="ti ti-reload"></i>Reload configuration</button></form>
|
<form method="post" action="#"><button type="submit" class="button"><i class="ti ti-reload"></i>Reload configuration</button></form>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="box" id="test-message" data-order="9">
|
||||||
|
<div class="sp-box-head"><span class="sp-no">09</span><h2>Test message</h2><div class="sp-end">Shows what the receiving server answers</div><a class="sp-help" href="help.html#test-message" title="What the test proves"><i class="ti ti-help-circle"></i></a></div>
|
||||||
|
<div class="sp-box-body">
|
||||||
|
<form class="sp-form" method="post" action="out-message.html">
|
||||||
|
<div class="field"><label class="label" for="rcpt">Recipient</label>
|
||||||
|
<div class="field has-addons">
|
||||||
|
<div class="control is-expanded has-icons-left"><input class="input" id="rcpt" name="recipient" type="email" placeholder="you@elsewhere.example" required><span class="icon is-left"><i class="ti ti-mail"></i></span></div>
|
||||||
|
<div class="control"><button type="submit" class="button is-primary"><i class="ti ti-send"></i>Send</button></div>
|
||||||
|
</div>
|
||||||
|
<p class="help">From <span class="sp-mono">postmaster@mail.example.org</span>, signed with the server's own key, through Postfix as its notifications go. It opens in the <a href="out-log.html">Outbound log</a>, where its page follows it to the receiving server. Ten an hour.</p></div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -14,6 +14,7 @@
|
|||||||
<ul class="menu-list">
|
<ul class="menu-list">
|
||||||
<li><a class="is-active" href="#checks"><i class="ti ti-activity-heartbeat"></i>Health checks</a></li>
|
<li><a class="is-active" href="#checks"><i class="ti ti-activity-heartbeat"></i>Health checks</a></li>
|
||||||
<li><a href="#certificate"><i class="ti ti-certificate"></i>TLS certificate</a></li>
|
<li><a href="#certificate"><i class="ti ti-certificate"></i>TLS certificate</a></li>
|
||||||
|
<li><a href="#test-message"><i class="ti ti-send"></i>Test message</a></li>
|
||||||
<li><a href="#rdns"><i class="ti ti-arrows-exchange"></i>Reverse DNS</a></li>
|
<li><a href="#rdns"><i class="ti ti-arrows-exchange"></i>Reverse DNS</a></li>
|
||||||
</ul>
|
</ul>
|
||||||
<p class="menu-label">Outbound</p>
|
<p class="menu-label">Outbound</p>
|
||||||
@@ -51,19 +52,20 @@
|
|||||||
<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, 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="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. <em>Your address</em>, in the Configuration card, is the address the panel sees your browser at: behind a reverse proxy it must be your own, taken from <code>X-Forwarded-For</code>; when it is the proxy's, every visitor shares one sign-in limit, and Health warns and names the address to put in <code>TRUSTED_PROXY_CIDR</code>.</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. Postfix serves its own copy of the mounted file and looks for a renewed one once a day by default; until it takes one up, the card shows it as a renewal waiting, and warns once it has waited longer than that check allows - the container log's cert-reload lines say why. It warns 14 days before expiry unless a renewal is already waiting, 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="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. Postfix serves its own copy of the mounted file and looks for a renewed one once a day by default; until it takes one up, the card shows it as a renewal waiting, and warns once it has waited longer than that check allows - the container log's cert-reload lines say why. It warns 14 days before expiry unless a renewal is already waiting, and fails once expired, or when the file is missing or unreadable — clients that verify certificates then refuse to connect. It also fails when none of its names is the hostname (<code>SELFPOST_HOSTNAME</code>) - the tag beside <em>Names</em> says which: a client that verifies the name refuses it however valid it is.</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 class="sp-help-topic" id="test-message"><h3>Test message</h3><p>A message from <code>postmaster@</code> the server's hostname to one address you type, sent the way notifications are: through Postfix, signed with the DKIM key of the server's own name. It proves the path from this server - TLS, its address's reputation, the SPF and DKIM records of its name - not your domains', which only mail from an application does. The page that opens is its row in the Outbound log: it shows whether the receiving server took it, put it off or refused it, with the lines of <code>mail.log</code>. Whether it then reached the inbox or the spam folder only the mailbox can say. The global role sends it, at most ten an hour.</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. <em>Greeting</em> is the line Postfix opens every connection with, <code>220</code> and its banner; it must name the hostname too - a receiver compares it with the name it looked up - and a container id or <code>localhost</code> there is a warning.</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. 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="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. A record DNS already holds as shown is folded to its name and status; Show values opens it to copy them again. The SPF check is shallow: it looks for this server's address in the record itself and does not follow <code>include:</code> or <code>redirect=</code>, so a record that relies on them is reported as a warning, not a failure. <code>p=none</code> does not affect delivery; tighten to <code>quarantine</code>, then <code>reject</code>, once the reports look clean. A domain's report address is set in its Domain settings.</p></div>
|
||||||
<div class="sp-help-topic" id="apps"><h3>Applications</h3><p>An application is one SASL login, unique across all domains. It may send as any address of its domain or only as listed addresses, and, with a list of client IPs set, only from those addresses. The password is shown once at creation or regeneration and is not stored in readable form.</p></div>
|
<div class="sp-help-topic" id="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 the panel does not show it again; the server keeps it, so a domain export can carry it.</p></div>
|
||||||
<div class="sp-help-topic" id="sender-check"><h3>Sender check</h3><p>An application sends only as itself, and that holds for both senders a message has. The envelope sender is held against the login by Postfix. The <code>From:</code> line, the one a mail program shows and the one DMARC holds the DKIM signature to, is held against it by the panel, under the same rule: an application set to any address of its domain may write any address of that domain, and one with a list of addresses only those. A message with any other <code>From:</code> - another of your domains, the server's own name, an address somewhere else, or none at all - is refused for good with <code>550 5.7.1</code>. The line must hold one address, read as a mail program reads it: what a display name around it says is not judged, and a line holding more than one address is refused. It is a rejected row of the Outbound log, and its page says which address was refused. This is what keeps one application from sending, under this server's signature, as another domain or another application.<br><br>The check is strict. While the panel does not answer, Postfix does not accept mail unchecked: it replies with a temporary error and the application sends again later. Server › Health counts what that costs: for how long the panel did not answer while Postfix itself was running; how many connections Postfix turned away because of it - it refuses from the connection on, before a message is offered, so a client that tries three times is three and a monitor that only connects is one; and how many messages were refused with a temporary error in the middle, when the panel stopped answering after a message had begun. A time marked ≈ includes a start at which supervisord could not say since when Postfix was running: the time since the panel's last sign of life is counted, which may be more than Postfix was up. The card warns when a connection was turned away or a message refused in the last 24 hours. If the numbers stay at nothing, the check is where it should be; if they do not, it belongs in a process of its own.</p></div>
|
<div class="sp-help-topic" id="sender-check"><h3>Sender check</h3><p>An application sends only as itself, and that holds for every sender a message names. The envelope sender is held against the login by Postfix. The <code>From:</code> line, the one a mail program shows and the one DMARC holds the DKIM signature to, is held against it by the panel, under the same rule: an application set to any address of its domain may write any address of that domain, and one with a list of addresses only those. A message with any other <code>From:</code> - another of your domains, the server's own name, an address somewhere else, or none at all - is refused for good with <code>550 5.7.1</code>. The line must hold one address, read as a mail program reads it: what a display name around it says is not judged, and a line holding more than one address is refused. A <code>Sender:</code> line, which a mail program shows beside <code>From:</code> as "on behalf of", is held to the same rule when the message has one. A system that writes for people outside your domains writes its own address in <code>From:</code> with the person's name, and the person's address in <code>Reply-To:</code>: their domain in <code>From:</code> would fail DMARC at the receiver anyway. It is a rejected row of the Outbound log, and its page says which address was refused. This is what keeps one application from sending, under this server's signature, as another domain or another application.<br><br>The check is strict. While the panel does not answer, Postfix does not accept mail unchecked: it replies with a temporary error and the application sends again later. Server › Health counts what that costs: for how long the panel did not answer while Postfix itself was running; how many connections Postfix turned away because of it - it refuses from the connection on, before a message is offered, so a client that tries three times is three and a monitor that only connects is one; and how many messages were refused with a temporary error in the middle, when the panel stopped answering after a message had begun. A time marked ≈ includes a start at which supervisord could not say since when Postfix was running: the time since the panel's last sign of life is counted, which may be more than Postfix was up. The card warns when a connection was turned away or a message refused in the last 24 hours. If the numbers stay at nothing, the check is where it should be; if they do not, it belongs in a process of its own.</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="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; or closed by the panel when Postfix no longer held the message and <code>mail.log</code> never said what became of it) and rejected (refused before queueing, so it never reached the queue: under a level-2 rate limit, from a client address the application does not allow, or for a <code>From:</code> line that is not the application's - the message's page says which). A message's page lists these steps and the lines Postfix wrote about it in <code>mail.log</code>, found by queue id - and for a message refused before it had one, Postfix's one line about the refusal. The lines are kept with the row: they stay on the page after that file has rotated, for as long as the row does. A message accepted before version 2.2.0 shows them only while they are still in the current file. Rows are deleted after the retention set in Server › Settings, 90 days by default, and their lines with them. A domain administrator sees only the rows of their own domains.</p></div>
|
<div class="sp-help-topic" id="log"><h3>Outbound log</h3><p>One row per message and recipient (a message refused for its From: or Sender: line is one row, however many recipients it had), 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; or closed by the panel when Postfix no longer held the message and <code>mail.log</code> never said what became of it) and rejected (refused before queueing, so it never reached the queue: under a level-2 rate limit, from a client address the application does not allow, or for a <code>From:</code> or <code>Sender:</code> line that is not the application's - the message's page says which). A message's page lists these steps and the lines Postfix wrote about it in <code>mail.log</code>, found by queue id - and for a message refused before it had one, Postfix's one line about the refusal. The lines are kept with the row: they stay on the page after that file has rotated, for as long as the row does. A message accepted before version 2.2.0 shows them only while they are still in the current file. Rows are deleted after the retention set in Server › Settings, 90 days by default, and their lines with them. 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="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 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 - its day is the reporter's, counted in UTC, and that is the day the lists and a report's title name, while the report's page gives its window in this panel's zone: 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>
|
||||||
<div class="box">
|
<div class="box">
|
||||||
<div class="sp-box-head"><span class="sp-no">03</span><h2>Inbound</h2></div>
|
<div class="sp-box-head"><span class="sp-no">03</span><h2>Inbound</h2></div>
|
||||||
@@ -71,19 +73,19 @@
|
|||||||
<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 sends only over TLS, and the mail waits in the queue while the upstream does not offer it or the TLS handshake fails - an upstream that answers STARTTLS with a permanent error bounces it; 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="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 sends only over TLS, and the mail waits in the queue while the upstream does not offer it or the TLS handshake fails - an upstream that answers STARTTLS with a permanent error bounces it; 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="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 refused with a temporary error, so the sending server tries again later, as the server's setting says; the box states which.</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 refused with a temporary error, so the sending server tries again later, as the server's setting says; the box states which.</p></div>
|
||||||
<div class="sp-help-topic" id="inbound-log"><h3>Inbound log</h3><p>One row per recipient of a message offered on port 25 for an inbound domain, with what was decided: <span class="tag is-success is-light">accept</span> — handed on to the upstream; <span class="tag is-danger is-light">reject</span> — refused for good; <span class="tag is-warning is-light">tempfail</span> — the sender was asked to come back later; <span class="tag is-primary is-light">quarantine</span> — held by the filter. <em>Why</em> names who decided. <code>rspamd</code> is the spam filter, with its action, the score against the reject threshold and the heaviest symbols; <code>postfix</code> is the relay's own rule — an unknown recipient, a sender domain that does not exist — with its reply; <code>selfpost</code> means nobody judged the message: it was relayed with no filter attached. Accept says what this server did, not what the upstream did with the message afterwards.<br><br>Accepted mail is recorded as it passes; refused mail is read from <code>mail.log</code>, so its row has no subject, and a message refused after its text was sent is one row however many recipients it had. A refusal to relay for a domain that is not an inbound one is every scanner on the internet and is not recorded. Rows are kept for the number of days set in Settings, and never more than 10 000.</p></div>
|
<div class="sp-help-topic" id="inbound-log"><h3>Inbound log</h3><p>One row per recipient of a message offered on port 25 for an inbound domain, with what was decided: <span class="tag is-success is-light">accept</span> — taken for the upstream; <span class="tag is-danger is-light">reject</span> — refused for good; <span class="tag is-warning is-light">tempfail</span> — the sender was asked to come back later; <span class="tag is-primary is-light">quarantine</span> — held by the filter. <em>Why</em> names who decided. <code>rspamd</code> is the spam filter, with its action, the score against the reject threshold and the heaviest symbols; <code>postfix</code> is the relay's own rule — an unknown recipient, a sender domain that does not exist — with its reply; <code>selfpost</code> means nobody judged the message: it was relayed with no filter attached. <em>Upstream</em> says what became of an accepted message: <span class="tag is-light">queued</span> — not tried yet; <span class="tag is-primary is-light">held</span> — held in the queue by the filter, not tried until it is released; <span class="tag is-success is-light">sent</span> — the upstream took it; <span class="tag is-warning is-light">deferred</span> — the upstream put it off or could not be reached, and Postfix tries again; <span class="tag is-danger is-light">bounced</span> — the upstream refused it, or it waited in the queue too long, and the sender was sent a bounce, if it had a return address. It opens the last reply and the host that gave it. A refused message has no upstream result, and neither has a row logged before 2.10.0.<br><br>Accepted mail is recorded as it passes; refused mail is read from <code>mail.log</code>, so its row has no subject, and a message refused after its text was sent is one row however many recipients it had. A refusal to relay for a domain that is not an inbound one is every scanner on the internet and is not recorded. Rows are kept for the number of days set in Settings, and never more than 10 000.</p></div>
|
||||||
<div class="sp-help-topic" id="filter-lists"><h3>Filter lists</h3><p>Two lists for the whole server that correct the spam filter by hand. An entry is a sender address, a sender domain — that domain, not its subdomains — or a client address or network in CIDR form. A sender on the Deny list is refused outright. A sender on the Allow list is given a weight no spam score outweighs, so the filter lets it through. The sender's address is whatever the sending server says it is - anyone can claim to be a sender you allow - so allow a client address or network where you can, and a sender address only for mail you would rather receive forged than lose. When entries of both lists match one message, <strong>Deny wins</strong>.<br><br>SelfPost does not apply the lists itself: it writes them as files under <code>/data/antispam/</code>, and rspamd rereads a file within seconds of a change. With the spam filter off the lists are kept and can be edited, but nothing reads them. The sender matched is the envelope sender — the one in the log — not the From line a mail program shows. Matched counts the messages in the Inbound log that the entry decided, so it reaches back as far as the log does; never means not once in that time, and such an entry is one to consider removing.</p></div>
|
<div class="sp-help-topic" id="filter-lists"><h3>Filter lists</h3><p>Two lists for the whole server that correct the spam filter by hand. An entry is a sender address, a sender domain — that domain, not its subdomains — or a client address or network in CIDR form. A sender on the Deny list is refused outright. A sender on the Allow list is given a weight no spam score outweighs, so the filter lets it through. The sender's address is whatever the sending server says it is - anyone can claim to be a sender you allow - so allow a client address or network where you can, and a sender address only for mail you would rather receive forged than lose. When entries of both lists match one message, <strong>Deny wins</strong>.<br><br>SelfPost does not apply the lists itself: it writes them as files under <code>/data/antispam/</code>, and rspamd rereads a file within seconds of a change. With the spam filter off the lists are kept and can be edited, but nothing reads them. The sender matched is the envelope sender — the one in the log — not the From line a mail program shows. Matched counts the messages in the Inbound log that the entry decided, so it reaches back as far as the log does; never means not once in that time, and such an entry is one to consider removing.</p></div>
|
||||||
</div>
|
</div>
|
||||||
<div class="box">
|
<div class="box">
|
||||||
<div class="sp-box-head"><span class="sp-no">04</span><h2>Server</h2></div>
|
<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 and application credentials), <code>docker-compose.yml</code>, <code>.env</code> and <code>certs/</code>. The Postfix queue is not in it. The backup is taken while the server is running, and Postfix keeps moving messages through the queue, so a copy could hold a message twice or miss it; mail still waiting for delivery is therefore not carried over, and a restored server starts with an empty queue. To keep the queue, stop the container and archive the project directory instead. The private key in <code>certs/</code> has to be readable by the panel: give it to group 10998 on the host (<code>chgrp 10998</code>, <code>chmod 640</code>). Until then the panel refuses to make a backup and names the file, rather than hand over an archive without the key. 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="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 and application credentials), <code>docker-compose.yml</code>, <code>.env</code> and <code>certs/</code>. Both it and a domain's export ask for your account password - the one you sign in with - at every download, so a session left open is not enough to take them; every attempt, right or wrong, counts against the limit of a password change, ten in 15 minutes. The Postfix queue is not in it. The backup is taken while the server is running, and Postfix keeps moving messages through the queue, so a copy could hold a message twice or miss it; mail still waiting for delivery is therefore not carried over, and a restored server starts with an empty queue. To keep the queue, stop the container and archive the project directory instead. The private key in <code>certs/</code> has to be readable by the panel: give it to group 10998 on the host (<code>chgrp 10998</code>, <code>chmod 640</code>). Until then the panel refuses to make a backup and names the file, rather than hand over an archive without the key. 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="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 everywhere; set for yourself, it keeps the session you set it from.</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. With the inbound relay on there is a second number, for the Inbound log, with the same range. 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. Notifications are turned on or off here for everyone.</p></div>
|
<div class="sp-help-topic" id="settings"><h3>Settings</h3><p>Settings holds what is true of the whole instance. The time zone is the container's, <code>TZ</code> in <code>.env</code> (UTC when it is not set): every time the panel shows and every day it counts is that zone's, as are the stamps Postfix writes in <code>mail.log</code>, so the two always agree; changing it is a restart of the container. The one exception is the day a DMARC report covers, which is the reporter's, counted in UTC. 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. With the inbound relay on there is a second number, for the Inbound log, with the same range. 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. Notifications are turned on or off here for everyone.</p></div>
|
||||||
<div class="sp-help-topic" id="notifications"><h3>Notifications</h3><p>The panel writes to its users when something needs them, from <code>postmaster@</code> the server's hostname. Four things are reported. A sending domain's SPF, DKIM or DMARC record stops matching: sent once two hourly checks in a row have found it so, and once more when the record matches again. A level-2 rate limit refuses mail: sent at the first refusal, then at most once a day for the same limit. The TLS certificate is 14 days from expiry, and again at 3 days if it is still the same certificate. A message has waited in the queue for more than three days, with one more message when none is left. A global user is told all of it; a domain administrator is told about the DNS records and limits of their own domains.<br><br>Each user turns notifications on or off under Account and needs an e-mail there; Server › Settings turns them off for everyone. They are the panel's own mail: not rows of the Outbound log, and not counted against any limit.<br><br>For that mail to arrive, the server's own name needs two DNS records of its own, as each of your domains does: a DKIM key, which the panel generates and signs notifications with, and an SPF record, <code>v=spf1 a -all</code>. Server › Settings gives both with the values to publish, and Server › Health shows whether they are in place. Without them notifications are still sent, and a receiver may file them as spam. Mail of your domains does not depend on either.</p></div>
|
<div class="sp-help-topic" id="notifications"><h3>Notifications</h3><p>The panel writes to its users when something needs them, from <code>postmaster@</code> the server's hostname. Four things are reported. A sending domain's SPF, DKIM or DMARC record stops matching: sent once two hourly checks in a row have found it so, and once more when the record matches again. A level-2 rate limit refuses mail: sent at the first refusal, then at most once a day for the same sending domain. The TLS certificate is 14 days from expiry, and again at 3 days if it is still the same certificate. A message has waited in the queue for more than three days, with one more message when none is left. A global user is told all of it; a domain administrator is told about the DNS records and limits of their own domains.<br><br>Each user turns notifications on or off under Account and needs an e-mail there; Server › Settings turns them off for everyone. They are the panel's own mail: not rows of the Outbound log, and not counted against any limit.<br><br>For that mail to arrive, the server's own name needs two DNS records of its own, as each of your domains does: a DKIM key, which the panel generates and signs notifications with, and an SPF record, <code>v=spf1 a -all</code>. Server › Settings gives both with the values to publish and whether they are in place, and its Re-check DNS checks them again once they are published; Server › Health shows them too. Without them notifications are still sent, and a receiver may file them as spam. Mail of your domains does not depend on either.</p></div>
|
||||||
</div>
|
</div>
|
||||||
<div class="box">
|
<div class="box">
|
||||||
<div class="sp-box-head"><span class="sp-no">05</span><h2>Account</h2></div>
|
<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, whether the panel sends you notifications, 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 class="sp-help-topic" id="account"><h3>Your account</h3><p>Account is your own, whatever your role: your username and e-mail, whether the panel sends you notifications, 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. A new e-mail is sent a link that confirms it, which works once, for 24 hours; changing the e-mail asks for your password. Notifications go to the address whether it is confirmed or not, but only a confirmed one can get a link to reset a forgotten password: <em>Forgot your password?</em> on the sign-in page sends it, for 30 minutes and once, and setting the new password signs every session of the account out. Without a confirmed e-mail the global administrator sets a new password under Users, and the only global administrator recovers from the container.</p></div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -35,8 +35,8 @@
|
|||||||
<div class="column is-9">
|
<div class="column is-9">
|
||||||
<div class="box" id="dns">
|
<div class="box" id="dns">
|
||||||
<div class="sp-box-head"><span class="sp-no">01</span><h2>MX record</h2><div class="sp-end">Checked 1 min ago</div><a class="sp-help" href="help.html#inbound" title="Backup-MX and forwarding"><i class="ti ti-help-circle"></i></a></div>
|
<div class="sp-box-head"><span class="sp-no">01</span><h2>MX record</h2><div class="sp-end">Checked 1 min ago</div><a class="sp-help" href="help.html#inbound" title="Backup-MX and forwarding"><i class="ti ti-help-circle"></i></a></div>
|
||||||
<div class="sp-record">
|
<details class="sp-record">
|
||||||
<div class="sp-record-head"><h3>MX</h3><span class="tag is-success is-light">published</span><small>Keep any existing primary MX if this is a backup-MX</small></div>
|
<summary class="sp-record-head"><h3>MX</h3><span class="tag is-success is-light">published</span><small>Keep any existing primary MX if this is a backup-MX</small><span class="sp-record-toggle"><i class="ti ti-eye"></i><span>Show values</span><span>Hide values</span></span></summary>
|
||||||
<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="lists.example.org"></div><div class="control"><button type="button" class="button"><i class="ti ti-copy"></i>Copy</button></div></div></div>
|
<div class="field has-addons"><div class="control is-expanded"><input class="input" readonly value="lists.example.org"></div><div class="control"><button type="button" class="button"><i class="ti ti-copy"></i>Copy</button></div></div></div>
|
||||||
@@ -44,7 +44,7 @@
|
|||||||
</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="10 mail.example.org."></div><div class="control"><button type="button" class="button"><i class="ti ti-copy"></i>Copy</button></div></div></div>
|
<div class="field has-addons"><div class="control is-expanded"><input class="input" readonly value="10 mail.example.org."></div><div class="control"><button type="button" class="button"><i class="ti ti-copy"></i>Copy</button></div></div></div>
|
||||||
</div>
|
</details>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="columns">
|
<div class="columns">
|
||||||
@@ -76,7 +76,7 @@
|
|||||||
dev@lists.example.org
|
dev@lists.example.org
|
||||||
owner@lists.example.org
|
owner@lists.example.org
|
||||||
security@lists.example.org</textarea></div>
|
security@lists.example.org</textarea></div>
|
||||||
<p class="help">Unknown recipients are rejected at RCPT, so this relay generates no backscatter. Prefer a list unless the upstream itself rejects unknowns.</p></div>
|
<p class="help">One per line or comma-separated. Unknown recipients are rejected at RCPT, so this relay generates no backscatter. Prefer a list unless the upstream itself rejects unknowns.</p></div>
|
||||||
<button type="submit" class="button is-primary">Save recipients</button>
|
<button type="submit" class="button is-primary">Save recipients</button>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
@@ -88,10 +88,9 @@ security@lists.example.org</textarea></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-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>Applies to</dt><dd>Every inbound domain of this server</dd></div>
|
||||||
<div><dt>Suspicious mail</dt><dd>Held — <a href="in-quarantine.html">1 message in quarantine</a> for this domain</dd></div>
|
<div><dt>Filter</dt><dd class="sp-mono">inet:antispam:11332</dd></div>
|
||||||
<div><dt>Spam</dt><dd>Refused at the door; see <em>reject</em> in the <a href="in-log.html">log</a></dd></div>
|
<div><dt>If the filter is down</dt><dd>Mail is accepted unfiltered</dd></div>
|
||||||
<div><dt>Allow and deny lists</dt><dd class="sp-muted">Kept by the global administrator, one pair for the whole server</dd></div>
|
|
||||||
</dl>
|
</dl>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -66,7 +66,7 @@
|
|||||||
<dl class="sp-facts">
|
<dl class="sp-facts">
|
||||||
<div><dt>Applied by</dt><dd>rspamd, from map files it rereads within seconds of a change</dd></div>
|
<div><dt>Applied by</dt><dd>rspamd, from map files it rereads within seconds of a change</dd></div>
|
||||||
<div><dt>Files</dt><dd><span class="sp-mono">/data/antispam/</span> — six, one for each list and kind of entry</dd></div>
|
<div><dt>Files</dt><dd><span class="sp-mono">/data/antispam/</span> — six, one for each list and kind of entry</dd></div>
|
||||||
<div><dt>Last written</dt><dd>2026-09-11 08:40 UTC</dd></div>
|
<div><dt>Last written</dt><dd>2026-09-11 08:40</dd></div>
|
||||||
<div><dt>When both lists match</dt><dd>Deny wins, whichever entries they are.</dd></div>
|
<div><dt>When both lists match</dt><dd>Deny wins, whichever entries they are.</dd></div>
|
||||||
</dl>
|
</dl>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
<div class="sp-grow">
|
<div class="sp-grow">
|
||||||
<p class="sp-kicker">Inbound</p>
|
<p class="sp-kicker">Inbound</p>
|
||||||
<h1 class="title is-3">Inbound log</h1>
|
<h1 class="title is-3">Inbound log</h1>
|
||||||
<p class="sp-lead">What port 25 decided about each message, one row per recipient. Accepted mail is recorded as it passes, refused mail is read from <code>mail.log</code>; the filter's verdict appears when the spam filter is on. Rows older than 90 days are deleted — change that in <a href="settings.html">Settings</a>.</p>
|
<p class="sp-lead">What port 25 decided about each message and what the upstream answered, one row per recipient. Accepted mail is recorded as it passes, refused mail is read from <code>mail.log</code>; the filter's verdict appears when the spam filter is on. Rows older than 90 days are deleted — change that in <a href="settings.html">Settings</a>.</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -20,15 +20,16 @@
|
|||||||
<a class="sp-help" href="help.html#inbound-log" title="What the decisions mean"><i class="ti ti-help-circle"></i></a>
|
<a class="sp-help" href="help.html#inbound-log" title="What the decisions 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>Decision</th><th></th></tr></thead>
|
<thead><tr><th>Time</th><th>From</th><th>To</th><th>Subject</th><th>Decision</th><th>Upstream</th><th></th></tr></thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
<tr><td class="sp-mono sp-muted sp-nowrap sp-small">09-21 14:00:18</td><td>t.berg@<wbr>uni.example<br><span class="sp-mono sp-muted sp-small">192.0.2.77</span></td><td>dev@<wbr>lists.example.org</td><td class="sp-clip">Re: [dev] patch series v3</td><td><button type="button" class="tag is-success is-light" popovertarget="why-1">accept</button><div class="sp-sheet" id="why-1" popover><div class="sp-sheet-in"><div class="sp-sheet-head"><span class="tag is-success is-light">accept</span><h2>Why</h2><button type="button" popovertarget="why-1" popovertargetaction="hide" aria-label="Close"><i class="ti ti-x"></i></button></div><div class="sp-sheet-body"><div class="table-container"><table class="table is-fullwidth"><tbody><tr><th>Decided by</th><td>rspamd</td></tr><tr><th>Verdict</th><td>no action, score 0.40 / 15.00</td></tr><tr><th>Subject</th><td>Re: [dev] patch series v3</td></tr></tbody></table></div></div></div></div></td><td class="sp-actions"><a href="in-filter-lists.html">Deny sender</a></td></tr>
|
<tr><td class="sp-mono sp-muted sp-nowrap sp-small">09-21 14:00:31</td><td>ci@<wbr>builds.example<br><span class="sp-mono sp-muted sp-small">192.0.2.90</span></td><td>dev@<wbr>lists.example.org</td><td class="sp-clip">[ci] nightly build passed</td><td><button type="button" class="tag is-success is-light" popovertarget="why-7">accept</button><div class="sp-sheet" id="why-7" popover><div class="sp-sheet-in"><div class="sp-sheet-head"><span class="tag is-success is-light">accept</span><h2>Why</h2><button type="button" popovertarget="why-7" popovertargetaction="hide" aria-label="Close"><i class="ti ti-x"></i></button></div><div class="sp-sheet-body"><div class="table-container"><table class="table is-fullwidth"><tbody><tr><th>Decided by</th><td>rspamd</td></tr><tr><th>Verdict</th><td>no action, score 1.10 / 15.00</td></tr><tr><th>Subject</th><td>[ci] nightly build passed</td></tr></tbody></table></div></div></div></div></td><td><span class="tag is-light">queued</span></td><td class="sp-actions"><a href="in-filter-lists.html">Deny sender</a></td></tr>
|
||||||
<tr><td class="sp-mono sp-muted sp-nowrap sp-small">09-21 13:59:02</td><td>invoice@<wbr>partner-billing.example<br><span class="sp-mono sp-muted sp-small">203.0.113.140</span></td><td>owner@<wbr>lists.example.org</td><td class="sp-clip">Outstanding invoice — action required</td><td><button type="button" class="tag is-success is-light" popovertarget="why-2">accept</button><div class="sp-sheet" id="why-2" popover><div class="sp-sheet-in"><div class="sp-sheet-head"><span class="tag is-success is-light">accept</span><h2>Why</h2><button type="button" popovertarget="why-2" popovertargetaction="hide" aria-label="Close"><i class="ti ti-x"></i></button></div><div class="sp-sheet-body"><div class="table-container"><table class="table is-fullwidth"><tbody><tr><th>Decided by</th><td>rspamd</td></tr><tr><th>Verdict</th><td>add header, score 9.80 / 15.00</td></tr><tr><th>Symbols</th><td class="sp-mono sp-small">PHISHING(5.00)<br>FORGED_SENDER(3.00)<br>R_SPF_FAIL(1.00)</td></tr><tr><th>Subject</th><td>Outstanding invoice — action required</td></tr></tbody></table></div></div></div></div></td><td class="sp-actions"><a href="in-filter-lists.html">Deny sender</a></td></tr>
|
<tr><td class="sp-mono sp-muted sp-nowrap sp-small">09-21 14:00:18</td><td>t.berg@<wbr>uni.example<br><span class="sp-mono sp-muted sp-small">192.0.2.77</span></td><td>dev@<wbr>lists.example.org</td><td class="sp-clip">Re: [dev] patch series v3</td><td><button type="button" class="tag is-success is-light" popovertarget="why-1">accept</button><div class="sp-sheet" id="why-1" popover><div class="sp-sheet-in"><div class="sp-sheet-head"><span class="tag is-success is-light">accept</span><h2>Why</h2><button type="button" popovertarget="why-1" popovertargetaction="hide" aria-label="Close"><i class="ti ti-x"></i></button></div><div class="sp-sheet-body"><div class="table-container"><table class="table is-fullwidth"><tbody><tr><th>Decided by</th><td>rspamd</td></tr><tr><th>Verdict</th><td>no action, score 0.40 / 15.00</td></tr><tr><th>Subject</th><td>Re: [dev] patch series v3</td></tr></tbody></table></div></div></div></div></td><td><button type="button" class="tag is-success is-light" popovertarget="up-1">sent</button><div class="sp-sheet" id="up-1" popover><div class="sp-sheet-in"><div class="sp-sheet-head"><span class="tag is-success is-light">sent</span><h2>Upstream</h2><button type="button" popovertarget="up-1" popovertargetaction="hide" aria-label="Close"><i class="ti ti-x"></i></button></div><div class="sp-sheet-body"><div class="table-container"><table class="table is-fullwidth"><tbody><tr><th>Host</th><td class="sp-mono sp-small">mx.upstream.example[198.51.100.5]:25</td></tr><tr><th>Reply</th><td class="sp-mono sp-small">250 2.0.0 Ok: queued as CE05D10261D</td></tr><tr><th>At</th><td class="sp-mono sp-small">09-21 14:00:19</td></tr></tbody></table></div></div></div></div></td><td class="sp-actions"><a href="in-filter-lists.html">Deny sender</a></td></tr>
|
||||||
<tr><td class="sp-mono sp-muted sp-nowrap sp-small">09-21 13:57:41</td><td>promo@<wbr>bulk.example<br><span class="sp-mono sp-muted sp-small">198.51.100.90</span></td><td>sales@<wbr>lists.example.org</td><td class="sp-clip sp-muted">—</td><td><button type="button" class="tag is-danger is-light" popovertarget="why-3">reject</button><div class="sp-sheet" id="why-3" popover><div class="sp-sheet-in"><div class="sp-sheet-head"><span class="tag is-danger is-light">reject</span><h2>Why</h2><button type="button" popovertarget="why-3" popovertargetaction="hide" aria-label="Close"><i class="ti ti-x"></i></button></div><div class="sp-sheet-body"><div class="table-container"><table class="table is-fullwidth"><tbody><tr><th>Decided by</th><td>postfix</td></tr><tr><th>Reply</th><td class="sp-mono sp-small">550 5.1.1 <sales@lists.example.org>: Recipient address rejected: User unknown in relay recipient table</td></tr></tbody></table></div></div></div></div></td><td class="sp-actions"></td></tr>
|
<tr><td class="sp-mono sp-muted sp-nowrap sp-small">09-21 13:59:02</td><td>invoice@<wbr>partner-billing.example<br><span class="sp-mono sp-muted sp-small">203.0.113.140</span></td><td>owner@<wbr>lists.example.org</td><td class="sp-clip">Outstanding invoice — action required</td><td><button type="button" class="tag is-success is-light" popovertarget="why-2">accept</button><div class="sp-sheet" id="why-2" popover><div class="sp-sheet-in"><div class="sp-sheet-head"><span class="tag is-success is-light">accept</span><h2>Why</h2><button type="button" popovertarget="why-2" popovertargetaction="hide" aria-label="Close"><i class="ti ti-x"></i></button></div><div class="sp-sheet-body"><div class="table-container"><table class="table is-fullwidth"><tbody><tr><th>Decided by</th><td>rspamd</td></tr><tr><th>Verdict</th><td>add header, score 9.80 / 15.00</td></tr><tr><th>Symbols</th><td class="sp-mono sp-small">PHISHING(5.00)<br>FORGED_SENDER(3.00)<br>R_SPF_FAIL(1.00)</td></tr><tr><th>Subject</th><td>Outstanding invoice — action required</td></tr></tbody></table></div></div></div></div></td><td><button type="button" class="tag is-danger is-light" popovertarget="up-2">bounced</button><div class="sp-sheet" id="up-2" popover><div class="sp-sheet-in"><div class="sp-sheet-head"><span class="tag is-danger is-light">bounced</span><h2>Upstream</h2><button type="button" popovertarget="up-2" popovertargetaction="hide" aria-label="Close"><i class="ti ti-x"></i></button></div><div class="sp-sheet-body"><div class="table-container"><table class="table is-fullwidth"><tbody><tr><th>Host</th><td class="sp-mono sp-small">mx.upstream.example[198.51.100.5]:25</td></tr><tr><th>Reply</th><td class="sp-mono sp-small">550 5.7.1 Message rejected as spam by the upstream filter</td></tr><tr><th>At</th><td class="sp-mono sp-small">09-21 13:59:04</td></tr><tr><th>Then</th><td>The sender was sent a bounce</td></tr></tbody></table></div></div></div></div></td><td class="sp-actions"><a href="in-filter-lists.html">Deny sender</a></td></tr>
|
||||||
<tr><td class="sp-mono sp-muted sp-nowrap sp-small">09-21 13:51:30</td><td>billing@<wbr>partner.example<br><span class="sp-mono sp-muted sp-small">198.51.100.12</span></td><td>owner@<wbr>lists.example.org</td><td class="sp-clip sp-muted">—</td><td><button type="button" class="tag is-danger is-light" popovertarget="why-4">reject</button><div class="sp-sheet" id="why-4" popover><div class="sp-sheet-in"><div class="sp-sheet-head"><span class="tag is-danger is-light">reject</span><h2>Why</h2><button type="button" popovertarget="why-4" popovertargetaction="hide" aria-label="Close"><i class="ti ti-x"></i></button></div><div class="sp-sheet-body"><div class="table-container"><table class="table is-fullwidth"><tbody><tr><th>Decided by</th><td>rspamd</td></tr><tr><th>Reply</th><td class="sp-mono sp-small">5.7.1 Spam message rejected</td></tr></tbody></table></div></div></div></div></td><td class="sp-actions"><a href="in-filter-lists.html">Allow sender</a></td></tr>
|
<tr><td class="sp-mono sp-muted sp-nowrap sp-small">09-21 13:57:41</td><td>promo@<wbr>bulk.example<br><span class="sp-mono sp-muted sp-small">198.51.100.90</span></td><td>sales@<wbr>lists.example.org</td><td class="sp-clip sp-muted">—</td><td><button type="button" class="tag is-danger is-light" popovertarget="why-3">reject</button><div class="sp-sheet" id="why-3" popover><div class="sp-sheet-in"><div class="sp-sheet-head"><span class="tag is-danger is-light">reject</span><h2>Why</h2><button type="button" popovertarget="why-3" popovertargetaction="hide" aria-label="Close"><i class="ti ti-x"></i></button></div><div class="sp-sheet-body"><div class="table-container"><table class="table is-fullwidth"><tbody><tr><th>Decided by</th><td>postfix</td></tr><tr><th>Reply</th><td class="sp-mono sp-small">550 5.1.1 <sales@lists.example.org>: Recipient address rejected: User unknown in relay recipient table</td></tr></tbody></table></div></div></div></div></td><td class="sp-muted">—</td><td class="sp-actions"></td></tr>
|
||||||
<tr><td class="sp-mono sp-muted sp-nowrap sp-small">09-21 13:44:02</td><td>hr@<wbr>partner.example<br><span class="sp-mono sp-muted sp-small">192.0.2.31</span></td><td>jobs@<wbr>acme.io</td><td class="sp-clip sp-muted">—</td><td><button type="button" class="tag is-warning is-light" popovertarget="why-5">tempfail</button><div class="sp-sheet" id="why-5" popover><div class="sp-sheet-in"><div class="sp-sheet-head"><span class="tag is-warning is-light">tempfail</span><h2>Why</h2><button type="button" popovertarget="why-5" popovertargetaction="hide" aria-label="Close"><i class="ti ti-x"></i></button></div><div class="sp-sheet-body"><div class="table-container"><table class="table is-fullwidth"><tbody><tr><th>Decided by</th><td>rspamd</td></tr><tr><th>Reply</th><td class="sp-mono sp-small">4.7.1 Try again later</td></tr></tbody></table></div></div></div></div></td><td class="sp-actions"><a href="in-filter-lists.html">Allow sender</a></td></tr>
|
<tr><td class="sp-mono sp-muted sp-nowrap sp-small">09-21 13:51:30</td><td>billing@<wbr>partner.example<br><span class="sp-mono sp-muted sp-small">198.51.100.12</span></td><td>owner@<wbr>lists.example.org</td><td class="sp-clip sp-muted">—</td><td><button type="button" class="tag is-danger is-light" popovertarget="why-4">reject</button><div class="sp-sheet" id="why-4" popover><div class="sp-sheet-in"><div class="sp-sheet-head"><span class="tag is-danger is-light">reject</span><h2>Why</h2><button type="button" popovertarget="why-4" popovertargetaction="hide" aria-label="Close"><i class="ti ti-x"></i></button></div><div class="sp-sheet-body"><div class="table-container"><table class="table is-fullwidth"><tbody><tr><th>Decided by</th><td>rspamd</td></tr><tr><th>Reply</th><td class="sp-mono sp-small">5.7.1 Spam message rejected</td></tr></tbody></table></div></div></div></div></td><td class="sp-muted">—</td><td class="sp-actions"><a href="in-filter-lists.html">Allow sender</a></td></tr>
|
||||||
<tr><td class="sp-mono sp-muted sp-nowrap sp-small">09-21 13:30:55</td><td>noreply@<wbr>github.example<br><span class="sp-mono sp-muted sp-small">192.0.2.200</span></td><td>security@<wbr>lists.example.org</td><td class="sp-clip">[advisory] new report</td><td><button type="button" class="tag is-success is-light" popovertarget="why-6">accept</button><div class="sp-sheet" id="why-6" popover><div class="sp-sheet-in"><div class="sp-sheet-head"><span class="tag is-success is-light">accept</span><h2>Why</h2><button type="button" popovertarget="why-6" popovertargetaction="hide" aria-label="Close"><i class="ti ti-x"></i></button></div><div class="sp-sheet-body"><div class="table-container"><table class="table is-fullwidth"><tbody><tr><th>Decided by</th><td>rspamd</td></tr><tr><th>Verdict</th><td>no action, score -98.10 / 15.00</td></tr><tr><th>Symbols</th><td class="sp-mono sp-small">SELFPOST_ALLOW_FROM(-100.00)<br>R_DKIM_ALLOW(-0.20)</td></tr><tr><th>Subject</th><td>[advisory] new report</td></tr></tbody></table></div></div></div></div></td><td class="sp-actions"><a href="in-filter-lists.html">Deny sender</a></td></tr>
|
<tr><td class="sp-mono sp-muted sp-nowrap sp-small">09-21 13:44:02</td><td>hr@<wbr>partner.example<br><span class="sp-mono sp-muted sp-small">192.0.2.31</span></td><td>jobs@<wbr>acme.io</td><td class="sp-clip sp-muted">—</td><td><button type="button" class="tag is-warning is-light" popovertarget="why-5">tempfail</button><div class="sp-sheet" id="why-5" popover><div class="sp-sheet-in"><div class="sp-sheet-head"><span class="tag is-warning is-light">tempfail</span><h2>Why</h2><button type="button" popovertarget="why-5" popovertargetaction="hide" aria-label="Close"><i class="ti ti-x"></i></button></div><div class="sp-sheet-body"><div class="table-container"><table class="table is-fullwidth"><tbody><tr><th>Decided by</th><td>rspamd</td></tr><tr><th>Reply</th><td class="sp-mono sp-small">4.7.1 Try again later</td></tr></tbody></table></div></div></div></div></td><td class="sp-muted">—</td><td class="sp-actions"><a href="in-filter-lists.html">Allow sender</a></td></tr>
|
||||||
|
<tr><td class="sp-mono sp-muted sp-nowrap sp-small">09-21 13:30:55</td><td>noreply@<wbr>github.example<br><span class="sp-mono sp-muted sp-small">192.0.2.200</span></td><td>security@<wbr>lists.example.org</td><td class="sp-clip">[advisory] new report</td><td><button type="button" class="tag is-success is-light" popovertarget="why-6">accept</button><div class="sp-sheet" id="why-6" popover><div class="sp-sheet-in"><div class="sp-sheet-head"><span class="tag is-success is-light">accept</span><h2>Why</h2><button type="button" popovertarget="why-6" popovertargetaction="hide" aria-label="Close"><i class="ti ti-x"></i></button></div><div class="sp-sheet-body"><div class="table-container"><table class="table is-fullwidth"><tbody><tr><th>Decided by</th><td>rspamd</td></tr><tr><th>Verdict</th><td>no action, score -98.10 / 15.00</td></tr><tr><th>Symbols</th><td class="sp-mono sp-small">SELFPOST_ALLOW_FROM(-100.00)<br>R_DKIM_ALLOW(-0.20)</td></tr><tr><th>Subject</th><td>[advisory] new report</td></tr></tbody></table></div></div></div></div></td><td><button type="button" class="tag is-warning is-light" popovertarget="up-6">deferred</button><div class="sp-sheet" id="up-6" popover><div class="sp-sheet-in"><div class="sp-sheet-head"><span class="tag is-warning is-light">deferred</span><h2>Upstream</h2><button type="button" popovertarget="up-6" popovertargetaction="hide" aria-label="Close"><i class="ti ti-x"></i></button></div><div class="sp-sheet-body"><div class="table-container"><table class="table is-fullwidth"><tbody><tr><th>Reply</th><td class="sp-mono sp-small">connect to mx.upstream.example[198.51.100.5]:25: Connection refused</td></tr><tr><th>At</th><td class="sp-mono sp-small">09-21 13:45:12</td></tr><tr><th>Then</th><td>Postfix tries again; the message is in the queue</td></tr></tbody></table></div></div></div></div></td><td class="sp-actions"><a href="in-filter-lists.html">Deny sender</a></td></tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
</table></div>
|
</table></div>
|
||||||
<div class="sp-box-foot"><span>Page 1 of 38</span><a href="#">Older →</a><span class="sp-end">A decision opens its reason · Allow / Deny sender opens <a href="in-filter-lists.html">Filter lists</a> with the entry filled in</span></div>
|
<div class="sp-box-foot"><span>Page 1 of 38</span><a href="#">Older →</a><span class="sp-end">A decision opens its reason, an upstream result its last reply · Allow / Deny sender opens <a href="in-filter-lists.html">Filter lists</a> with the entry filled in</span></div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -15,7 +15,7 @@
|
|||||||
<dl class="sp-facts">
|
<dl class="sp-facts">
|
||||||
<div><dt>Inbound domain</dt><dd><a href="in-domain.html">lists.example.org</a></dd></div>
|
<div><dt>Inbound domain</dt><dd><a href="in-domain.html">lists.example.org</a></dd></div>
|
||||||
<div><dt>Client</dt><dd class="sp-mono">203.0.113.140</dd></div>
|
<div><dt>Client</dt><dd class="sp-mono">203.0.113.140</dd></div>
|
||||||
<div><dt>Held</dt><dd>2026-09-21 13:59:02 UTC</dd></div>
|
<div><dt>Held</dt><dd>2026-09-21 13:59:02</dd></div>
|
||||||
<div><dt>Deleted on</dt><dd>2026-10-05 · in 14 days</dd></div>
|
<div><dt>Deleted on</dt><dd>2026-10-05 · in 14 days</dd></div>
|
||||||
<div><dt>Size</dt><dd>48 kB · 1 attachment (<span class="sp-mono">invoice.html</span>)</dd></div>
|
<div><dt>Size</dt><dd>48 kB · 1 attachment (<span class="sp-mono">invoice.html</span>)</dd></div>
|
||||||
<div><dt>Would go to</dt><dd class="sp-mono">mx.internal:25</dd></div>
|
<div><dt>Would go to</dt><dd class="sp-mono">mx.internal:25</dd></div>
|
||||||
@@ -40,13 +40,14 @@
|
|||||||
|
|
||||||
<div class="box">
|
<div class="box">
|
||||||
<div class="sp-box-head"><span class="sp-no">03</span><h2>Headers</h2><div class="sp-end">The body and attachments are never rendered in the panel</div></div>
|
<div class="sp-box-head"><span class="sp-no">03</span><h2>Headers</h2><div class="sp-end">The body and attachments are never rendered in the panel</div></div>
|
||||||
<pre class="sp-log"><span class="sp-d">Received:</span> from mail.partner-billing.example (unknown [203.0.113.140]) by mail.example.org
|
<pre class="sp-log"><span class="sp-ll"><span class="sp-d">Received:</span> from mail.partner-billing.example (unknown [203.0.113.140]) by mail.example.org
|
||||||
<span class="sp-d">From:</span> "Partner Billing" <invoice@partner-billing.example>
|
</span><span class="sp-ll"><span class="sp-d">From:</span> "Partner Billing" <invoice@partner-billing.example>
|
||||||
<span class="sp-d">Reply-To:</span> <span class="sp-w">payments.dept@freemail.example</span>
|
</span><span class="sp-ll"><span class="sp-d">Reply-To:</span> <span class="sp-w">payments.dept@freemail.example</span>
|
||||||
<span class="sp-d">To:</span> owner@lists.example.org
|
</span><span class="sp-ll"><span class="sp-d">To:</span> owner@lists.example.org
|
||||||
<span class="sp-d">Subject:</span> Outstanding invoice — action required
|
</span><span class="sp-ll"><span class="sp-d">Subject:</span> Outstanding invoice — action required
|
||||||
<span class="sp-d">Date:</span> Mon, 21 Sep 2026 13:58:57 +0000
|
</span><span class="sp-ll"><span class="sp-d">Date:</span> Mon, 21 Sep 2026 13:58:57 +0000
|
||||||
<span class="sp-d">Authentication-Results:</span> <span class="sp-e">spf=fail dkim=none dmarc=fail</span></pre>
|
</span><span class="sp-ll"><span class="sp-d">Authentication-Results:</span> <span class="sp-e">spf=fail dkim=none dmarc=fail</span>
|
||||||
|
</span></pre>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="columns">
|
<div class="columns">
|
||||||
|
|||||||
@@ -16,7 +16,7 @@
|
|||||||
</form>
|
</form>
|
||||||
</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>Held, UTC</th><th>From</th><th>To</th><th>Subject</th><th>Why</th><th>Size</th><th>Deleted in</th><th></th></tr></thead>
|
<thead><tr><th>Held</th><th>From</th><th>To</th><th>Subject</th><th>Why</th><th>Size</th><th>Deleted in</th><th></th></tr></thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
<tr><td class="sp-mono sp-muted sp-nowrap sp-small">09-21 13:59:02</td><td>invoice@<wbr>partner-billing.example</td><td>owner@<wbr>lists.example.org</td><td class="sp-clip"><a href="in-quarantine-message.html"><strong>Outstanding invoice — action required</strong></a></td><td class="sp-muted sp-small">score 9.8 · PHISHING</td><td class="sp-muted sp-nowrap">48 kB</td><td class="sp-nowrap">14 days</td><td class="sp-actions"><a href="in-quarantine-message.html">Review</a></td></tr>
|
<tr><td class="sp-mono sp-muted sp-nowrap sp-small">09-21 13:59:02</td><td>invoice@<wbr>partner-billing.example</td><td>owner@<wbr>lists.example.org</td><td class="sp-clip"><a href="in-quarantine-message.html"><strong>Outstanding invoice — action required</strong></a></td><td class="sp-muted sp-small">score 9.8 · PHISHING</td><td class="sp-muted sp-nowrap">48 kB</td><td class="sp-nowrap">14 days</td><td class="sp-actions"><a href="in-quarantine-message.html">Review</a></td></tr>
|
||||||
<tr><td class="sp-mono sp-muted sp-nowrap sp-small">09-20 22:14:40</td><td>no-reply@<wbr>shipping-update.example</td><td>jobs@<wbr>acme.io</td><td class="sp-clip"><a href="in-quarantine-message.html"><strong>Your parcel could not be delivered</strong></a></td><td class="sp-muted sp-small">score 8.1 · URL_SHORTENER</td><td class="sp-muted sp-nowrap">12 kB</td><td class="sp-nowrap">13 days</td><td class="sp-actions"><a href="in-quarantine-message.html">Review</a></td></tr>
|
<tr><td class="sp-mono sp-muted sp-nowrap sp-small">09-20 22:14:40</td><td>no-reply@<wbr>shipping-update.example</td><td>jobs@<wbr>acme.io</td><td class="sp-clip"><a href="in-quarantine-message.html"><strong>Your parcel could not be delivered</strong></a></td><td class="sp-muted sp-small">score 8.1 · URL_SHORTENER</td><td class="sp-muted sp-nowrap">12 kB</td><td class="sp-nowrap">13 days</td><td class="sp-actions"><a href="in-quarantine-message.html">Review</a></td></tr>
|
||||||
|
|||||||
@@ -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">Refreshes every 10 s</div></div>
|
<div class="sp-box-head"><span class="sp-no">01</span><h2>Waiting</h2><div class="sp-end">Refreshes on its own</div></div>
|
||||||
<div class="sp-empty"><i class="ti ti-stack-2"></i>Nothing is waiting. Every accepted message has reached its upstream.</div>
|
<div class="sp-empty"><i class="ti ti-stack-2"></i>Nothing is waiting. Every accepted message has reached its upstream.</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -12,6 +12,6 @@
|
|||||||
<h1 class="title is-3">No administrator yet</h1>
|
<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>
|
<p class="sp-muted">Open the one-time setup link printed in the container log to create one.</p>
|
||||||
</div>
|
</div>
|
||||||
<p class="sp-foot">© 2026 Mix · <a href="#">License (AGPL-3.0)</a> · <a href="#">Source</a> · No warranty</p>
|
<p class="sp-foot">Copyright © 2026 Mikhail Yenuchenko · <a href="#">License (AGPL-3.0)</a> · <a href="#">Source</a> · No warranty</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -0,0 +1,27 @@
|
|||||||
|
<!-- 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">
|
||||||
|
<form class="sp-form" method="post" action="overview.html">
|
||||||
|
<p class="sp-kicker">mail.example.org</p>
|
||||||
|
<h1 class="title is-3">Sign in</h1>
|
||||||
|
<div class="notification is-success is-light"><i class="ti ti-circle-check"></i>If that account has a confirmed e-mail, the link is on its way. It works once, for 30 minutes.</div>
|
||||||
|
<div class="field">
|
||||||
|
<label class="label" for="username">Username</label>
|
||||||
|
<div class="control has-icons-left"><input class="input" id="username" name="username" autocomplete="username" required><span class="icon is-left"><i class="ti ti-user"></i></span></div>
|
||||||
|
</div>
|
||||||
|
<div class="field">
|
||||||
|
<label class="label" for="password">Password</label>
|
||||||
|
<div class="control has-icons-left"><input class="input" id="password" name="password" type="password" autocomplete="current-password" required><span class="icon is-left"><i class="ti ti-lock"></i></span></div>
|
||||||
|
</div>
|
||||||
|
<div class="field mt-5"><button type="submit" class="button is-primary is-fullwidth">Sign in</button></div>
|
||||||
|
<p class="mt-4"><a href="login-reset.html">Forgot your password?</a></p>
|
||||||
|
</form>
|
||||||
|
<p class="sp-foot">Copyright © 2026 Mikhail Yenuchenko · <a href="#">License (AGPL-3.0)</a> · <a href="#">Source</a> · No warranty</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
<!-- title: New password | 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">
|
||||||
|
<form class="sp-form" method="post" action="login.html">
|
||||||
|
<p class="sp-kicker">mail.example.org · admin</p>
|
||||||
|
<h1 class="title is-3">New password</h1>
|
||||||
|
<p class="sp-muted mb-4">Setting it signs every session of this account out; sign in with it afterwards.</p>
|
||||||
|
<div class="field">
|
||||||
|
<label class="label" for="new_password">New password</label>
|
||||||
|
<div class="control has-icons-left"><input class="input" id="new_password" name="new_password" type="password" autocomplete="new-password" required><span class="icon is-left"><i class="ti ti-lock"></i></span></div>
|
||||||
|
</div>
|
||||||
|
<div class="field">
|
||||||
|
<label class="label" for="new_password_confirm">Repeat</label>
|
||||||
|
<div class="control has-icons-left"><input class="input" id="new_password_confirm" name="new_password_confirm" type="password" autocomplete="new-password" required><span class="icon is-left"><i class="ti ti-lock"></i></span></div>
|
||||||
|
</div>
|
||||||
|
<div class="field mt-5"><button type="submit" class="button is-primary is-fullwidth">Set password</button></div>
|
||||||
|
</form>
|
||||||
|
<p class="sp-foot">Copyright © 2026 Mikhail Yenuchenko · <a href="#">License (AGPL-3.0)</a> · <a href="#">Source</a> · No warranty</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
<!-- title: Reset password | 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">
|
||||||
|
<form class="sp-form" method="post" action="login-reset-sent.html">
|
||||||
|
<p class="sp-kicker">mail.example.org</p>
|
||||||
|
<h1 class="title is-3">Reset password</h1>
|
||||||
|
<p class="sp-muted mb-4">A link to set a new password goes to the e-mail of the account, once that address is confirmed under Account. It works once, for 30 minutes.</p>
|
||||||
|
<div class="field">
|
||||||
|
<label class="label" for="username">Username</label>
|
||||||
|
<div class="control has-icons-left"><input class="input" id="username" name="username" autocomplete="username" required><span class="icon is-left"><i class="ti ti-user"></i></span></div>
|
||||||
|
</div>
|
||||||
|
<div class="field mt-5"><button type="submit" class="button is-primary is-fullwidth">Send the link</button></div>
|
||||||
|
<p class="mt-4"><a href="login.html">Back to sign in</a></p>
|
||||||
|
</form>
|
||||||
|
<p class="sp-foot">Copyright © 2026 Mikhail Yenuchenko · <a href="#">License (AGPL-3.0)</a> · <a href="#">Source</a> · No warranty</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
@@ -19,7 +19,8 @@
|
|||||||
<div class="control has-icons-left"><input class="input" id="password" name="password" type="password" autocomplete="current-password" required><span class="icon is-left"><i class="ti ti-lock"></i></span></div>
|
<div class="control has-icons-left"><input class="input" id="password" name="password" type="password" autocomplete="current-password" required><span class="icon is-left"><i class="ti ti-lock"></i></span></div>
|
||||||
</div>
|
</div>
|
||||||
<div class="field mt-5"><button type="submit" class="button is-primary is-fullwidth">Sign in</button></div>
|
<div class="field mt-5"><button type="submit" class="button is-primary is-fullwidth">Sign in</button></div>
|
||||||
|
<p class="mt-4"><a href="login-reset.html">Forgot your password?</a></p>
|
||||||
</form>
|
</form>
|
||||||
<p class="sp-foot">© 2026 Mix · <a href="#">License (AGPL-3.0)</a> · <a href="#">Source</a> · No warranty</p>
|
<p class="sp-foot">Copyright © 2026 Mikhail Yenuchenko · <a href="#">License (AGPL-3.0)</a> · <a href="#">Source</a> · No warranty</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
<div class="sp-grow">
|
<div class="sp-grow">
|
||||||
<p class="sp-kicker"><a href="out-domains.html">Outbound domains</a> / <a href="out-domain.html">example.org</a> / Applications /</p>
|
<p class="sp-kicker"><a href="out-domains.html">Outbound domains</a> / <a href="out-domain.html">example.org</a> / Applications /</p>
|
||||||
<h1 class="title is-3 sp-mono">prod-server</h1>
|
<h1 class="title is-3 sp-mono">prod-server</h1>
|
||||||
<p class="sp-lead">1 102 messages in 30 days · peak 96 msg/h · average 1.5 msg/h. The same form, empty, adds a new application.</p>
|
<p class="sp-lead">1 102 messages in 30 days · peak 96 msg/h · average 1.5 msg/h.</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="buttons">
|
<div class="buttons">
|
||||||
<a class="button" href="out-app-created.html"><i class="ti ti-key"></i>New password</a>
|
<a class="button" href="out-app-created.html"><i class="ti ti-key"></i>New password</a>
|
||||||
@@ -19,7 +19,7 @@
|
|||||||
<div class="sp-box-body">
|
<div class="sp-box-body">
|
||||||
<div class="field"><label class="label" for="login">Login</label>
|
<div class="field"><label class="label" for="login">Login</label>
|
||||||
<div class="control"><input class="input sp-mono" id="login" name="login" value="prod-server" autocomplete="off" spellcheck="false" required></div>
|
<div class="control"><input class="input sp-mono" id="login" name="login" value="prod-server" autocomplete="off" spellcheck="false" required></div>
|
||||||
<p class="help">Unique across domains; letters, digits, <code>.</code> <code>-</code> <code>_</code>.</p></div>
|
<p class="help">Fixed: add a new application to use another login.</p></div>
|
||||||
<div class="field"><label class="label" for="amode">May send as</label>
|
<div class="field"><label class="label" for="amode">May send as</label>
|
||||||
<div class="control"><div class="select is-fullwidth"><select id="amode" name="mode"><option selected>Any address of the domain</option><option>Specific addresses (list)</option></select></div></div></div>
|
<div class="control"><div class="select is-fullwidth"><select id="amode" name="mode"><option selected>Any address of the domain</option><option>Specific addresses (list)</option></select></div></div></div>
|
||||||
<div class="field"><label class="label" for="addresses">Addresses</label>
|
<div class="field"><label class="label" for="addresses">Addresses</label>
|
||||||
|
|||||||
@@ -38,6 +38,8 @@
|
|||||||
<div class="field"><label class="label" for="encpw2">Repeat password</label><div class="control"><input class="input" id="encpw2" type="password"></div></div>
|
<div class="field"><label class="label" for="encpw2">Repeat password</label><div class="control"><input class="input" id="encpw2" type="password"></div></div>
|
||||||
</div>
|
</div>
|
||||||
<p class="help mb-3">Keep this password: SelfPost does not store it, and the file cannot be opened without it.</p>
|
<p class="help mb-3">Keep this password: SelfPost does not store it, and the file cannot be opened without it.</p>
|
||||||
|
<div class="field"><label class="label" for="acpw">Your account password</label><div class="control"><input class="input" id="acpw" name="account_password" type="password" autocomplete="current-password" required></div>
|
||||||
|
<p class="help">The one you sign in with, asked at every export: the file holds the domain's DKIM key and its applications' credentials.</p></div>
|
||||||
<button type="submit" class="button"><i class="ti ti-download"></i>Export domain</button>
|
<button type="submit" class="button"><i class="ti ti-download"></i>Export domain</button>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
@@ -51,9 +53,9 @@
|
|||||||
<form class="sp-form" method="post" action="#">
|
<form class="sp-form" method="post" action="#">
|
||||||
<div class="field"><label class="label" for="mode">Limit mode</label>
|
<div class="field"><label class="label" for="mode">Limit mode</label>
|
||||||
<div class="control"><div class="select is-fullwidth"><select id="mode" name="mode"><option>Manual</option><option selected>Auto (from statistics)</option></select></div></div></div>
|
<div class="control"><div class="select is-fullwidth"><select id="mode" name="mode"><option>Manual</option><option selected>Auto (from statistics)</option></select></div></div></div>
|
||||||
<div class="field"><label class="label" for="mult">Multiplier (1.5–10, default 3)</label>
|
<div class="field"><label class="label" for="mult">Multiplier (1.5–5, default 2.5)</label>
|
||||||
<div class="control"><input class="input" id="mult" name="auto_multiplier" type="number" min="1.5" max="10" step="0.1" value="3"></div>
|
<div class="control"><input class="input" id="mult" name="auto_multiplier" type="number" min="1.5" max="10" step="0.1" value="3"></div>
|
||||||
<p class="help">Computed limit: <strong>288</strong> messages / 3600 s. Peak was 96 msg/h. Last recalculated yesterday 03:00 UTC.</p></div>
|
<p class="help">Computed limit: <strong>288</strong> messages / 3600 s. Peak was 96 msg/h. Last recalculated yesterday 03:00.</p></div>
|
||||||
<div class="buttons">
|
<div class="buttons">
|
||||||
<button type="submit" class="button is-primary">Save limit</button>
|
<button type="submit" class="button is-primary">Save limit</button>
|
||||||
<button type="submit" class="button" formaction="#recalc">Recalculate now</button>
|
<button type="submit" class="button" formaction="#recalc">Recalculate now</button>
|
||||||
|
|||||||
@@ -40,8 +40,8 @@
|
|||||||
<div class="box" id="dns">
|
<div class="box" id="dns">
|
||||||
<div class="sp-box-head"><span class="sp-no">01</span><h2>DNS records</h2><div class="sp-end">Checked 3 min ago</div><a class="sp-help" href="help.html#dns" title="What to publish and why"><i class="ti ti-help-circle"></i></a></div>
|
<div class="sp-box-head"><span class="sp-no">01</span><h2>DNS records</h2><div class="sp-end">Checked 3 min ago</div><a class="sp-help" href="help.html#dns" title="What to publish and why"><i class="ti ti-help-circle"></i></a></div>
|
||||||
|
|
||||||
<div class="sp-record">
|
<details class="sp-record">
|
||||||
<div class="sp-record-head"><h3>DKIM</h3><span class="tag is-success is-light">published</span><small>Not a secret · selector <code>sp2026</code></small></div>
|
<summary class="sp-record-head"><h3>DKIM</h3><span class="tag is-success is-light">published</span><small>Not a secret · selector <code>sp2026</code></small><span class="sp-record-toggle"><i class="ti ti-eye"></i><span>Show values</span><span>Hide values</span></span></summary>
|
||||||
<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="sp2026._domainkey.example.org"></div><div class="control"><button type="button" class="button"><i class="ti ti-copy"></i>Copy</button></div></div></div>
|
<div class="field has-addons"><div class="control is-expanded"><input class="input" readonly value="sp2026._domainkey.example.org"></div><div class="control"><button type="button" class="button"><i class="ti ti-copy"></i>Copy</button></div></div></div>
|
||||||
@@ -49,7 +49,7 @@
|
|||||||
</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"><textarea class="textarea" rows="3" readonly>v=DKIM1; k=rsa; p=MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAx3kqV0mJd8e2f1Yb7Qw5uT9sLr4nHc6pZaXo1vKe0yWm2gBt8RjN5dCq3hUf7lPs9aEi4oMz6xTb1kGv0nYw…IDAQAB</textarea></div><div class="control"><button type="button" class="button"><i class="ti ti-copy"></i>Copy</button></div></div></div>
|
<div class="field has-addons"><div class="control is-expanded"><textarea class="textarea" rows="3" readonly>v=DKIM1; k=rsa; p=MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAx3kqV0mJd8e2f1Yb7Qw5uT9sLr4nHc6pZaXo1vKe0yWm2gBt8RjN5dCq3hUf7lPs9aEi4oMz6xTb1kGv0nYw…IDAQAB</textarea></div><div class="control"><button type="button" class="button"><i class="ti ti-copy"></i>Copy</button></div></div></div>
|
||||||
</div>
|
</details>
|
||||||
|
|
||||||
<div class="sp-record">
|
<div class="sp-record">
|
||||||
<div class="sp-record-head"><h3>SPF</h3><span class="tag is-danger is-light">mismatch</span><small>Merge into the existing record — never publish a second one</small></div>
|
<div class="sp-record-head"><h3>SPF</h3><span class="tag is-danger is-light">mismatch</span><small>Merge into the existing record — never publish a second one</small></div>
|
||||||
@@ -65,8 +65,8 @@
|
|||||||
<p class="help is-danger">The published record does not authorize 203.0.113.25. Add <code>ip4:203.0.113.25</code> before <code>~all</code>. Shallow check: <code>include:</code> is not followed.</p></div>
|
<p class="help is-danger">The published record does not authorize 203.0.113.25. Add <code>ip4:203.0.113.25</code> before <code>~all</code>. Shallow check: <code>include:</code> is not followed.</p></div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="sp-record">
|
<details class="sp-record">
|
||||||
<div class="sp-record-head"><h3>DMARC</h3><span class="tag is-success is-light">published</span><small>Report address: <a href="out-domain-settings.html">example.org@dmarc.mail.example.org</a></small></div>
|
<summary 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><span class="sp-record-toggle"><i class="ti ti-eye"></i><span>Show values</span><span>Hide values</span></span></summary>
|
||||||
<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>
|
||||||
@@ -75,7 +75,7 @@
|
|||||||
<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: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>
|
<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>
|
</details>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="box" id="apps">
|
<div class="box" id="apps">
|
||||||
|
|||||||
@@ -21,7 +21,7 @@
|
|||||||
<a class="sp-help" href="help.html#log" title="What the statuses mean"><i class="ti ti-help-circle"></i></a>
|
<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</th><th>From</th><th>To</th><th>Subject</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>shop@<wbr>shop.example.org</td><td>m.keller@<wbr>gmx.de</td><td class="sp-clip">Your order #48213 has shipped</td><td><span class="tag is-success is-light">sent</span></td><td class="sp-actions"><a href="out-message.html">Details</a></td></tr>
|
<tr><td class="sp-mono sp-muted sp-nowrap sp-small">09-21 14:01:52</td><td>shop@<wbr>shop.example.org</td><td>m.keller@<wbr>gmx.de</td><td class="sp-clip">Your order #48213 has shipped</td><td><span class="tag is-success is-light">sent</span></td><td class="sp-actions"><a href="out-message.html">Details</a></td></tr>
|
||||||
<tr><td class="sp-mono sp-muted sp-nowrap sp-small">09-21 14:01:50</td><td>shop@<wbr>shop.example.org</td><td>anna.lind@<wbr>outlook.com</td><td class="sp-clip">Your order #48212 has shipped</td><td><span class="tag is-success is-light">sent</span></td><td class="sp-actions"><a href="out-message.html">Details</a></td></tr>
|
<tr><td class="sp-mono sp-muted sp-nowrap sp-small">09-21 14:01:50</td><td>shop@<wbr>shop.example.org</td><td>anna.lind@<wbr>outlook.com</td><td class="sp-clip">Your order #48212 has shipped</td><td><span class="tag is-success is-light">sent</span></td><td class="sp-actions"><a href="out-message.html">Details</a></td></tr>
|
||||||
|
|||||||
@@ -15,8 +15,8 @@
|
|||||||
<dl class="sp-facts">
|
<dl class="sp-facts">
|
||||||
<div><dt>Domain</dt><dd><a href="out-domain.html">example.org</a></dd></div>
|
<div><dt>Domain</dt><dd><a href="out-domain.html">example.org</a></dd></div>
|
||||||
<div><dt>Application</dt><dd class="sp-mono"><a href="out-app.html">alerts</a></dd></div>
|
<div><dt>Application</dt><dd class="sp-mono"><a href="out-app.html">alerts</a></dd></div>
|
||||||
<div><dt>Accepted</dt><dd>2026-09-21 13:58:07 UTC</dd></div>
|
<div><dt>Accepted</dt><dd>2026-09-21 13:58:07</dd></div>
|
||||||
<div><dt>Status reported</dt><dd>2026-09-21 13:58:09 UTC</dd></div>
|
<div><dt>Status reported</dt><dd>2026-09-21 13:58:09</dd></div>
|
||||||
<div><dt>Queue id</dt><dd class="sp-mono">4XcB7k2Jm9z1</dd></div>
|
<div><dt>Queue id</dt><dd class="sp-mono">4XcB7k2Jm9z1</dd></div>
|
||||||
<div><dt>Journal id</dt><dd class="sp-mono">184220</dd></div>
|
<div><dt>Journal id</dt><dd class="sp-mono">184220</dd></div>
|
||||||
</dl>
|
</dl>
|
||||||
@@ -28,9 +28,9 @@
|
|||||||
<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-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>2026-09-21 13:58:07</time><strong>Accepted and queued</strong> — Postfix accepted the message over an authenticated submission and the journal-milter recorded it.</li>
|
||||||
<li class="sp-warn"><time>13:58:09 UTC</time><strong>Deferred</strong> — the receiving server asked to retry later</li>
|
<li class="sp-warn"><time>2026-09-21 13:58:09</time><strong>Deferred, will be retried</strong> — the receiving server could not take the message yet. Postfix retries: first after 5 minutes, then with increasing gaps up to 1 hour 7 minutes, for up to 5 days.</li>
|
||||||
<li class="sp-pending"><time>next attempt ≈ 14:08 UTC</time>Still in the <a href="out-queue.html">queue</a>; kept for 5 days, then bounced</li>
|
<li class="sp-pending"><time>not yet</time>Still in the queue; the next attempt is Postfix's own, and after 5 days it is bounced.</li>
|
||||||
</ol>
|
</ol>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -39,8 +39,9 @@
|
|||||||
|
|
||||||
<div class="box">
|
<div class="box">
|
||||||
<div class="sp-box-head"><span class="sp-no">03</span><h2>Delivery log</h2><div class="sp-end">Lines from <code>mail.log</code> with this queue id</div></div>
|
<div class="sp-box-head"><span class="sp-no">03</span><h2>Delivery log</h2><div class="sp-end">Lines from <code>mail.log</code> with this queue id</div></div>
|
||||||
<pre class="sp-log"><span class="sp-d">13:58:07</span> postfix/smtpd[2114]: 4XcB7k2Jm9z1: client=unknown[198.51.100.7], sasl_username=alerts
|
<pre class="sp-log"><span class="sp-ll"><span class="sp-d">13:58:07</span> postfix/smtpd[2114]: 4XcB7k2Jm9z1: client=unknown[198.51.100.7], sasl_username=alerts
|
||||||
<span class="sp-d">13:58:07</span> postfix/cleanup[2120]: 4XcB7k2Jm9z1: message-id=<a81f0c@alertmanager>
|
</span><span class="sp-ll"><span class="sp-d">13:58:07</span> postfix/cleanup[2120]: 4XcB7k2Jm9z1: message-id=<a81f0c@alertmanager>
|
||||||
<span class="sp-d">13:58:07</span> postfix/qmgr[2061]: 4XcB7k2Jm9z1: from=<alerts@example.org>, size=2214, nrcpt=1 (queue active)
|
</span><span class="sp-ll"><span class="sp-d">13:58:07</span> postfix/qmgr[2061]: 4XcB7k2Jm9z1: from=<alerts@example.org>, size=2214, nrcpt=1 (queue active)
|
||||||
<span class="sp-d">13:58:09</span> <span class="sp-w">postfix/smtp[2123]: 4XcB7k2Jm9z1: to=<noc@example.org>, relay=mx1.example.org[192.0.2.10]:25, delay=1.9, status=deferred (host mx1.example.org said: 451 4.7.1 Greylisted, try again in 5 minutes)</span></pre>
|
</span><span class="sp-ll"><span class="sp-d">13:58:09</span> <span class="sp-w">postfix/smtp[2123]: 4XcB7k2Jm9z1: to=<noc@example.org>, relay=mx1.example.org[192.0.2.10]:25, delay=1.9, status=deferred (host mx1.example.org said: 451 4.7.1 Greylisted, try again in 5 minutes)</span>
|
||||||
|
</span></pre>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -3,14 +3,14 @@
|
|||||||
<div class="sp-grow">
|
<div class="sp-grow">
|
||||||
<p class="sp-kicker">Outbound</p>
|
<p class="sp-kicker">Outbound</p>
|
||||||
<h1 class="title is-3">Outbound queue</h1>
|
<h1 class="title is-3">Outbound queue</h1>
|
||||||
<p class="sp-lead">Messages accepted from applications and still waiting for the receiving server. Refreshes every 10 s.</p>
|
<p class="sp-lead">Messages accepted from applications and still waiting for the receiving server. Refreshes on its own.</p>
|
||||||
</div>
|
</div>
|
||||||
</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><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="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</th><th>From</th><th>To</th><th>Last reply</th><th></th></tr></thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
<tr><td class="sp-mono">4XcB7k2Jm9z1</td><td class="sp-mono sp-muted sp-nowrap sp-small">09-21 13:58:07</td><td>alerts@<wbr>example.org</td><td>noc@<wbr>example.org</td><td class="sp-muted">451 4.7.1 Greylisted, try again in 5 minutes</td><td class="sp-actions"><a href="out-message.html">Details</a></td></tr>
|
<tr><td class="sp-mono">4XcB7k2Jm9z1</td><td class="sp-mono sp-muted sp-nowrap sp-small">09-21 13:58:07</td><td>alerts@<wbr>example.org</td><td>noc@<wbr>example.org</td><td class="sp-muted">451 4.7.1 Greylisted, try again in 5 minutes</td><td class="sp-actions"><a href="out-message.html">Details</a></td></tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
|
|||||||
@@ -4,9 +4,9 @@
|
|||||||
<div class="sp-grow">
|
<div class="sp-grow">
|
||||||
<p class="sp-kicker">mail.example.org</p>
|
<p class="sp-kicker">mail.example.org</p>
|
||||||
<h1 class="title is-3">Mail is flowing. One thing needs attention.</h1>
|
<h1 class="title is-3">Mail is flowing. One thing needs attention.</h1>
|
||||||
<p class="sp-lead">The TLS certificate expires in 12 days and has not renewed yet.</p>
|
<p class="sp-lead"><a href="health.html">TLS certificate</a>: expires in 12 days.</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="sp-muted sp-small">Updated 14:02:31 UTC<br>refreshes every 10 s</div>
|
<div class="sp-muted sp-small">Updated 14:02:31<br>refreshes on its own</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="box">
|
<div class="box">
|
||||||
@@ -16,7 +16,7 @@
|
|||||||
<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"><span class="sp-h-icon"><i class="ti ti-server-cog"></i></span><span><span class="sp-h-name">Processes</span><span class="sp-h-value">5 of 5 running</span><span class="sp-h-sub">up 18 days</span></span></a>
|
||||||
<a href="health.html" class="sp-warn"><span class="sp-h-icon"><i class="ti ti-certificate"></i></span><span><span class="sp-h-name">TLS certificate</span><span class="sp-h-value">Expires in 12 days</span><span class="sp-h-sub">renewal has not run</span></span></a>
|
<a href="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"><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">5 of 5 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">6 of 6 answering</span><span class="sp-h-sub">incl. the spam filter</span></span></a>
|
||||||
<a href="health.html"><span class="sp-h-icon"><i class="ti ti-arrows-exchange"></i></span><span><span class="sp-h-name">Reverse DNS</span><span class="sp-h-value">Forward = reverse</span><span class="sp-h-sub">203.0.113.25</span></span></a>
|
<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>
|
||||||
|
|||||||
@@ -10,7 +10,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="box">
|
<div class="box">
|
||||||
<div class="sp-box-head"><span class="sp-no">01</span><h2>Checks</h2><div class="sp-end">Ran 14:02 UTC · 3.1 s</div></div>
|
<div class="sp-box-head"><span class="sp-no">01</span><h2>Checks</h2><div class="sp-end">Ran 14:02 · 3.1 s</div></div>
|
||||||
<div class="table-container"><table class="table is-fullwidth">
|
<div class="table-container"><table class="table is-fullwidth">
|
||||||
<thead><tr><th>Check</th><th>Result</th><th>Found</th><th>What to do</th></tr></thead>
|
<thead><tr><th>Check</th><th>Result</th><th>Found</th><th>What to do</th></tr></thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
@@ -42,7 +42,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="column is-6">
|
<div class="column is-6">
|
||||||
<dl class="sp-facts">
|
<dl class="sp-facts">
|
||||||
<div><dt>Last test</dt><dd>14:03 UTC to <span class="sp-mono">mix@elsewhere.example</span></dd></div>
|
<div><dt>Last test</dt><dd>14:03 to <span class="sp-mono">mix@elsewhere.example</span></dd></div>
|
||||||
<div><dt>Result</dt><dd><span class="tag is-success is-light">queued</span> <span class="sp-mono sp-small">4XcC1d8Pq2z1</span></dd></div>
|
<div><dt>Result</dt><dd><span class="tag is-success is-light">queued</span> <span class="sp-mono sp-small">4XcC1d8Pq2z1</span></dd></div>
|
||||||
<div><dt>Then</dt><dd><a href="out-message.html">Follow it in the outbound log</a></dd></div>
|
<div><dt>Then</dt><dd><a href="out-message.html">Follow it in the outbound log</a></dd></div>
|
||||||
</dl>
|
</dl>
|
||||||
|
|||||||
@@ -5,11 +5,14 @@
|
|||||||
<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 and password 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 class="buttons">
|
||||||
|
<form method="post" action="#"><button type="submit" class="button"><i class="ti ti-refresh"></i>Re-check DNS</button></form>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<form class="sp-form" method="post" action="#">
|
<div class="columns">
|
||||||
<div class="columns">
|
<div class="column is-6">
|
||||||
<div class="column is-6">
|
<form class="sp-form" method="post" action="#">
|
||||||
<div class="box">
|
<div class="box">
|
||||||
<div class="sp-box-head"><span class="sp-no">01</span><h2>Log retention</h2><a class="sp-help" href="help.html#settings" title="How long rows are kept"><i class="ti ti-help-circle"></i></a></div>
|
<div class="sp-box-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">
|
||||||
@@ -22,53 +25,63 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="box" id="rate-limits">
|
|
||||||
<div class="sp-box-head"><span class="sp-no">02</span><h2>Sending rate limits</h2><div class="sp-end">Read-only</div><a class="sp-help" href="help.html#limits" title="Levels 1 and 2"><i class="ti ti-help-circle"></i></a></div>
|
|
||||||
<div class="sp-box-body">
|
|
||||||
<dl class="sp-facts">
|
|
||||||
<div><dt>Level 1 · per client IP</dt><dd class="sp-big">600 / h</dd><dd class="sp-muted">Set in <code>.env</code>; restart to change. Nothing below may exceed it.</dd></div>
|
|
||||||
<div><dt>Level 2 · domain, application</dt><dd>Set on each <a href="out-domain-settings.html">domain</a> and <a href="out-app.html">application</a>.</dd></div>
|
|
||||||
</dl>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="column is-6">
|
|
||||||
<div class="box" id="notifications">
|
<div class="box" id="notifications">
|
||||||
<div class="sp-box-head"><span class="sp-no">03</span><h2>Notifications</h2><a class="sp-help" href="help.html#notifications" title="What is sent and when"><i class="ti ti-help-circle"></i></a></div>
|
<div class="sp-box-head"><span class="sp-no">02</span><h2>Notifications</h2><a class="sp-help" href="help.html#notifications" title="What is sent and when"><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="notify" checked> Send notification e-mails</label>
|
<div class="field"><label class="checkbox"><input type="checkbox" name="notify" checked> Send notification e-mails</label>
|
||||||
<p class="help">From <span class="sp-mono">postmaster@mail.example.org</span>, to every user who has an e-mail and has not turned them off under <a href="account.html">Account</a>.</p></div>
|
<p class="help">From <span class="sp-mono">postmaster@mail.example.org</span>, to every user who has an e-mail and has not turned them off under <a href="account.html">Account</a>.</p></div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="buttons"><button type="submit" class="button is-primary">Save settings</button></div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="box" id="notification-mail">
|
<div class="column is-6">
|
||||||
<div class="sp-box-head"><span class="sp-no">04</span><h2>Notification mail</h2><div class="sp-end">DNS records of the server's own name</div><a class="sp-help" href="help.html#notifications" title="What the panel sends and what makes it arrive"><i class="ti ti-help-circle"></i></a></div>
|
<div class="box" id="notification-mail">
|
||||||
|
<div class="sp-box-head"><span class="sp-no">03</span><h2>Notification mail</h2><div class="sp-end">The server's own name · checked 3 min ago</div><a class="sp-help" href="help.html#notifications" title="What the panel sends and what makes it arrive"><i class="ti ti-help-circle"></i></a></div>
|
||||||
|
|
||||||
<div class="sp-record">
|
<details class="sp-record">
|
||||||
<div class="sp-record-head"><h3>DKIM</h3><span class="tag is-success is-light">published</span><small>Not a secret · selector <code>sp2026</code></small></div>
|
<summary class="sp-record-head"><h3>DKIM</h3><span class="tag is-success is-light">published</span><small>Not a secret · selector <code>sp2026</code></small><span class="sp-record-toggle"><i class="ti ti-eye"></i><span>Show values</span><span>Hide values</span></span></summary>
|
||||||
<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="sp2026._domainkey.mail.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="sp2026._domainkey.mail.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 class="field"><label class="label">Value to publish</label>
|
|
||||||
<div class="field has-addons"><div class="control is-expanded"><textarea class="textarea" rows="5" readonly>v=DKIM1; k=rsa; p=MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAq7n2Lw0c5vXeR1dYt8uJb3mKs6pHf9aZoWg4iNy2TqCx0lVjE7rUh5sBk1dMa8fPz3oGc6yXw9tIe4nQv2uL…IDAQAB</textarea></div><div class="control"><button type="button" class="button"><i class="ti ti-copy"></i>Copy</button></div></div></div>
|
|
||||||
</div>
|
</div>
|
||||||
|
<div class="field"><label class="label">Value to publish</label>
|
||||||
|
<div class="field has-addons"><div class="control is-expanded"><textarea class="textarea" rows="5" readonly>v=DKIM1; k=rsa; p=MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAq7n2Lw0c5vXeR1dYt8uJb3mKs6pHf9aZoWg4iNy2TqCx0lVjE7rUh5sBk1dMa8fPz3oGc6yXw9tIe4nQv2uL…IDAQAB</textarea></div><div class="control"><button type="button" class="button"><i class="ti ti-copy"></i>Copy</button></div></div></div>
|
||||||
|
</details>
|
||||||
|
|
||||||
<div class="sp-record">
|
<div class="sp-record">
|
||||||
<div class="sp-record-head"><h3>SPF</h3><span class="tag is-warning is-light">missing</span><small>For the server's own name, not for your domains</small></div>
|
<div class="sp-record-head"><h3>SPF</h3><span class="tag is-warning is-light">missing</span><small>For the server's own name, not for your domains</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="mail.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="mail.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 class="field"><label class="label">Value to publish</label>
|
|
||||||
<div class="field has-addons"><div class="control is-expanded"><input class="input" readonly value="v=spf1 a -all"></div><div class="control"><button type="button" class="button"><i class="ti ti-copy"></i>Copy</button></div></div></div>
|
|
||||||
<p class="help">Notifications are sent without it, and a receiver may file them as spam.</p>
|
|
||||||
</div>
|
</div>
|
||||||
|
<div class="field"><label class="label">Value to publish</label>
|
||||||
|
<div class="field has-addons"><div class="control is-expanded"><input class="input" readonly value="v=spf1 a -all"></div><div class="control"><button type="button" class="button"><i class="ti ti-copy"></i>Copy</button></div></div></div>
|
||||||
|
<p class="help">Notifications are sent without it, and a receiver may file them as spam.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="box" id="rate-limits">
|
||||||
|
<div class="sp-box-head"><span class="sp-no">04</span><h2>Sending rate limits</h2><div class="sp-end">Read-only</div><a class="sp-help" href="help.html#limits" title="Levels 1 and 2"><i class="ti ti-help-circle"></i></a></div>
|
||||||
|
<div class="sp-box-body">
|
||||||
|
<dl class="sp-facts">
|
||||||
|
<div><dt>Level 1 · per client IP</dt><dd class="sp-big">600 / h</dd><dd class="sp-muted">Set in <code>.env</code>; restart to change. Nothing below may exceed it.</dd></div>
|
||||||
|
<div><dt>Level 2 · domain, application</dt><dd>Set on each <a href="out-domain-settings.html">domain</a> and <a href="out-app.html">application</a>.</dd></div>
|
||||||
|
</dl>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="box" id="time-zone">
|
||||||
|
<div class="sp-box-head"><span class="sp-no">05</span><h2>Time zone</h2><div class="sp-end">Read-only</div><a class="sp-help" href="help.html#settings" title="Times and days of the panel"><i class="ti ti-help-circle"></i></a></div>
|
||||||
|
<div class="sp-box-body">
|
||||||
|
<dl class="sp-facts">
|
||||||
|
<div><dt>Zone</dt><dd class="sp-mono">Europe/Moscow</dd><dd class="sp-muted">UTC+03:00. Set in <code>.env</code> (<code>TZ</code>); restart to change.</dd></div>
|
||||||
|
</dl>
|
||||||
|
<p class="mt-4 sp-muted">Every time the panel shows, and every day it counts - today on Health, the days of the statistics - is this zone's, and so are the stamps of <code>mail.log</code>. One exception: the day a DMARC report covers is the reporter's, in UTC.</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="buttons"><button type="submit" class="button is-primary">Save settings</button></div>
|
</div>
|
||||||
</form>
|
|
||||||
|
|||||||
@@ -25,6 +25,6 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="field mt-5"><button type="submit" class="button is-primary is-fullwidth">Create administrator</button></div>
|
<div class="field mt-5"><button type="submit" class="button is-primary is-fullwidth">Create administrator</button></div>
|
||||||
</form>
|
</form>
|
||||||
<p class="sp-foot">© 2026 Mix · <a href="#">License (AGPL-3.0)</a> · <a href="#">Source</a> · No warranty</p>
|
<p class="sp-foot">Copyright © 2026 Mikhail Yenuchenko · <a href="#">License (AGPL-3.0)</a> · <a href="#">Source</a> · No warranty</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -3,19 +3,20 @@
|
|||||||
<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">System log</h1>
|
<h1 class="title is-3">System log</h1>
|
||||||
<p class="sp-lead">The tail of <code>mail.log</code>, the log Postfix writes. For one message, open it from a log page instead.</p>
|
<p class="sp-lead">The tail of <code>mail.log</code>, the log Postfix writes. For one message, open it from the <a href="out-log.html">Outbound log</a> instead.</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="box">
|
<div class="box">
|
||||||
<div class="sp-box-head"><span class="sp-no">01</span><h2>Recent lines</h2><div class="sp-end">Last 200 · refreshes every 10 s</div></div>
|
<div class="sp-box-head"><span class="sp-no">01</span><h2>Recent lines</h2><div class="sp-end">Last 200 lines · refreshes on its own</div></div>
|
||||||
<pre class="sp-log"><span class="sp-d">Sep 21 14:01:52</span> postfix/smtp[2131]: 4XcB9q0Lw2z1: to=<m.keller@gmx.de>, relay=mx00.gmx.net[212.227.15.9]:25, delay=0.8, status=sent (250 Requested mail action okay)
|
<pre class="sp-log"><span class="sp-ll"><span class="sp-d">Sep 21 14:01:52</span> postfix/smtp[2131]: 4XcB9q0Lw2z1: to=<m.keller@gmx.de>, relay=mx00.gmx.net[212.227.15.9]:25, delay=0.8, status=sent (250 Requested mail action okay)
|
||||||
<span class="sp-d">Sep 21 14:01:52</span> postfix/qmgr[58]: 4XcB9q0Lw2z1: removed
|
</span><span class="sp-ll"><span class="sp-d">Sep 21 14:01:52</span> postfix/qmgr[58]: 4XcB9q0Lw2z1: removed
|
||||||
<span class="sp-d">Sep 21 14:00:18</span> postfix/smtpd[2129]: connect from mail.uni.example[192.0.2.77]
|
</span><span class="sp-ll"><span class="sp-d">Sep 21 14:00:18</span> postfix/smtpd[2129]: connect from mail.uni.example[192.0.2.77]
|
||||||
<span class="sp-d">Sep 21 14:00:18</span> postfix/smtp[2130]: 4XcB8z5Tn1z1: to=<dev@lists.example.org>, relay=mx.internal[10.0.4.2]:25, status=sent (250 2.0.0 Ok: queued)
|
</span><span class="sp-ll"><span class="sp-d">Sep 21 14:00:18</span> postfix/smtp[2130]: 4XcB8z5Tn1z1: to=<dev@lists.example.org>, relay=mx.internal[10.0.4.2]:25, status=sent (250 2.0.0 Ok: queued)
|
||||||
<span class="sp-d">Sep 21 13:58:09</span> <span class="sp-w">postfix/smtp[2123]: 4XcB7k2Jm9z1: to=<noc@example.org>, relay=mx1.example.org[192.0.2.10]:25, status=deferred (451 4.7.1 Greylisted, try again in 5 minutes)</span>
|
</span><span class="sp-ll"><span class="sp-d">Sep 21 13:58:09</span> <span class="sp-w">postfix/smtp[2123]: 4XcB7k2Jm9z1: to=<noc@example.org>, relay=mx1.example.org[192.0.2.10]:25, status=deferred (451 4.7.1 Greylisted, try again in 5 minutes)</span>
|
||||||
<span class="sp-d">Sep 21 13:57:41</span> <span class="sp-e">postfix/smtpd[2119]: NOQUEUE: reject: RCPT from bulk.example[198.51.100.90]: 550 5.1.1 <sales@lists.example.org>: Recipient address rejected: unknown recipient</span>
|
</span><span class="sp-ll"><span class="sp-d">Sep 21 13:57:41</span> <span class="sp-e">postfix/smtpd[2119]: NOQUEUE: reject: RCPT from bulk.example[198.51.100.90]: 550 5.1.1 <sales@lists.example.org>: Recipient address rejected: unknown recipient</span>
|
||||||
<span class="sp-d">Sep 21 13:44:02</span> <span class="sp-w">postfix/smtp[2101]: 4XcB2m7Hd4z1: to=<jobs@acme.io>, relay=none, status=deferred (connect to 10.0.4.12[10.0.4.12]:2525: Connection timed out)</span>
|
</span><span class="sp-ll"><span class="sp-d">Sep 21 13:44:02</span> <span class="sp-w">postfix/smtp[2101]: 4XcB2m7Hd4z1: to=<jobs@acme.io>, relay=none, status=deferred (connect to 10.0.4.12[10.0.4.12]:2525: Connection timed out)</span>
|
||||||
<span class="sp-d">Sep 21 13:41:19</span> <span class="sp-e">postfix/smtp[2098]: 4XcB1c3Rk8z1: to=<j.doe@nonexistent.example>, status=bounced (Host or domain name not found)</span>
|
</span><span class="sp-ll"><span class="sp-d">Sep 21 13:41:19</span> <span class="sp-e">postfix/smtp[2098]: 4XcB1c3Rk8z1: to=<j.doe@nonexistent.example>, status=bounced (Host or domain name not found)</span>
|
||||||
<span class="sp-d">Sep 21 13:40:02</span> <span class="sp-w">journal-milter[40]: rate limit: app=prod-server 201/200 in 3600s — tempfail at MAIL FROM</span></pre>
|
</span><span class="sp-ll"><span class="sp-d">Sep 21 13:40:02</span> <span class="sp-w">journal-milter[40]: rate limit: app=prod-server 201/200 in 3600s — tempfail at MAIL FROM</span>
|
||||||
|
</span></pre>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -14,9 +14,9 @@
|
|||||||
<p class="mb-3">Deleting the panel user <strong>acme-ops</strong> will:</p>
|
<p class="mb-3">Deleting the panel user <strong>acme-ops</strong> will:</p>
|
||||||
<ul class="sp-list-check">
|
<ul class="sp-list-check">
|
||||||
<li><i class="ti ti-x"></i>end any signed-in session of this user at once;</li>
|
<li><i class="ti ti-x"></i>end any signed-in session of this user at once;</li>
|
||||||
<li><i class="ti ti-x"></i>remove the assignments to <strong>all outbound domains</strong> and to <strong>acme.io</strong> (inbound).</li>
|
<li><i class="ti ti-x"></i>remove the assignments to <strong>all outbound domains</strong> (outbound) and to <strong>acme.io</strong> (inbound).</li>
|
||||||
</ul>
|
</ul>
|
||||||
<p class="sp-muted mb-4">The domains, their applications and passwords, upstreams and recipients are not affected.</p>
|
<p class="sp-muted mb-4">Applications and their passwords, upstreams and recipients are not affected.</p>
|
||||||
<form method="post" action="users.html">
|
<form method="post" action="users.html">
|
||||||
<div class="buttons">
|
<div class="buttons">
|
||||||
<button type="submit" class="button is-danger"><i class="ti ti-trash"></i>Delete acme-ops</button>
|
<button type="submit" class="button is-danger"><i class="ti ti-trash"></i>Delete acme-ops</button>
|
||||||
|
|||||||
@@ -3,7 +3,6 @@
|
|||||||
<div class="sp-grow">
|
<div class="sp-grow">
|
||||||
<p class="sp-kicker"><a href="users.html">Users</a> /</p>
|
<p class="sp-kicker"><a href="users.html">Users</a> /</p>
|
||||||
<h1 class="title is-3">acme-ops</h1>
|
<h1 class="title is-3">acme-ops</h1>
|
||||||
<p class="sp-lead">The same form, empty, creates a new user.</p>
|
|
||||||
</div>
|
</div>
|
||||||
<a class="button is-danger is-light" href="user-delete.html"><i class="ti ti-trash"></i>Delete user</a>
|
<a class="button is-danger is-light" href="user-delete.html"><i class="ti ti-trash"></i>Delete user</a>
|
||||||
</div>
|
</div>
|
||||||
@@ -15,8 +14,8 @@
|
|||||||
<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-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 <span class="tag is-success is-light">confirmed</span></label><div class="control"><input class="input" id="email" name="email" type="email" value="ops@acme.io" autocomplete="off"></div>
|
||||||
<p class="help">For notifications and a password-reset link. The user can change it under Account.</p></div>
|
<p class="help">The user's own address. They can change it under Account. A new one is sent a link to confirm it; until it is confirmed no reset link goes to it.</p></div>
|
||||||
<div class="field"><label class="label" for="password">Password</label><div class="control"><input class="input" id="password" name="password" type="password" autocomplete="new-password"></div>
|
<div class="field"><label class="label" for="password">Password</label><div class="control"><input class="input" id="password" name="password" type="password" autocomplete="new-password"></div>
|
||||||
<p class="help">Leave empty to keep the current one.</p></div>
|
<p class="help">Leave empty to keep the current one.</p></div>
|
||||||
<div class="field"><label class="label" for="role">Role</label>
|
<div class="field"><label class="label" for="role">Role</label>
|
||||||
@@ -33,7 +32,7 @@
|
|||||||
<div class="field"><label class="checkbox"><input type="checkbox" name="domain_ids" checked disabled> example.org</label></div>
|
<div class="field"><label class="checkbox"><input type="checkbox" name="domain_ids" checked disabled> example.org</label></div>
|
||||||
<div class="field"><label class="checkbox"><input type="checkbox" name="domain_ids" checked disabled> shop.example.org</label></div>
|
<div class="field"><label class="checkbox"><input type="checkbox" name="domain_ids" checked disabled> shop.example.org</label></div>
|
||||||
<div class="field"><label class="checkbox"><input type="checkbox" name="domain_ids" checked disabled> notify.acme.io</label></div>
|
<div class="field"><label class="checkbox"><input type="checkbox" name="domain_ids" checked disabled> notify.acme.io</label></div>
|
||||||
<p class="help">With <em>All</em> ticked the list below it is ignored — and greyed out where scripts run.</p>
|
<p class="help">With <em>All</em> ticked the single domains are ignored.</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="box">
|
<div class="box">
|
||||||
|
|||||||
@@ -12,8 +12,8 @@
|
|||||||
<body>
|
<body>
|
||||||
<nav class="navbar is-primary" aria-label="main navigation">
|
<nav class="navbar is-primary" aria-label="main navigation">
|
||||||
<div class="container">
|
<div class="container">
|
||||||
<div class="navbar-brand"><a class="navbar-item" href="overview.html"><img src="../../selfpost-wordmark.svg" alt="SelfPost"></a></div>
|
<div class="navbar-brand"><a class="navbar-item" href="overview.html"><img src="../../selfpost-wordmark.svg" alt="SelfPost"></a><details class="sp-touchnav"><summary>Menu</summary><div class="sp-touchnav-panel"><a href="overview.html">Overview</a><details class="sp-touchnav-group"><summary>Outbound</summary><a href="out-domains.html"><i class="ti ti-world-upload"></i>Domains</a><a href="out-log.html"><i class="ti ti-list-details"></i>Log</a><a href="out-queue.html"><i class="ti ti-stack-2"></i>Queue</a><a href="dmarc.html"><i class="ti ti-report-analytics"></i>DMARC reports</a></details><details class="sp-touchnav-group"><summary>Inbound</summary><a href="in-domains.html"><i class="ti ti-world-download"></i>Domains</a><a href="in-log.html"><i class="ti ti-list-details"></i>Log</a><a href="in-queue.html"><i class="ti ti-stack-2"></i>Queue</a><a href="in-quarantine.html"><i class="ti ti-mail-pause"></i>Quarantine</a><a href="in-filter-lists.html"><i class="ti ti-filter"></i>Filter lists</a></details><details class="sp-touchnav-group" open><summary>Server</summary><a href="health.html"><i class="ti ti-heartbeat"></i>Health</a><a href="preflight.html"><i class="ti ti-checklist"></i>Preflight</a><a href="system-log.html" aria-current="page"><i class="ti ti-file-text"></i>System log</a><a href="backup.html"><i class="ti ti-archive"></i>Backup</a><a href="users.html"><i class="ti ti-users"></i>Users</a><a href="settings.html"><i class="ti ti-settings"></i>Settings</a></details><details class="sp-touchnav-group"><summary><i class="ti ti-user-circle"></i>admin</summary><a href="account.html"><i class="ti ti-key"></i>Account</a><a href="help.html"><i class="ti ti-help"></i>Help</a><form method="post" action="login.html"><button type="submit"><i class="ti ti-logout"></i>Sign out</button></form></details></div></details></div>
|
||||||
<div class="navbar-menu is-active">
|
<div class="navbar-menu">
|
||||||
<div class="navbar-start">
|
<div class="navbar-start">
|
||||||
<a class="navbar-item" href="overview.html">Overview</a>
|
<a class="navbar-item" href="overview.html">Overview</a>
|
||||||
<div class="navbar-item has-dropdown is-hoverable">
|
<div class="navbar-item has-dropdown is-hoverable">
|
||||||
@@ -69,24 +69,25 @@
|
|||||||
<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">System log</h1>
|
<h1 class="title is-3">System log</h1>
|
||||||
<p class="sp-lead">The tail of <code>mail.log</code>, the log Postfix writes. For one message, open it from a log page instead.</p>
|
<p class="sp-lead">The tail of <code>mail.log</code>, the log Postfix writes. For one message, open it from the <a href="out-log.html">Outbound log</a> instead.</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="box">
|
<div class="box">
|
||||||
<div class="sp-box-head"><span class="sp-no">01</span><h2>Recent lines</h2><div class="sp-end">Last 200 · refreshes every 10 s</div></div>
|
<div class="sp-box-head"><span class="sp-no">01</span><h2>Recent lines</h2><div class="sp-end">Last 200 lines · refreshes on its own</div></div>
|
||||||
<pre class="sp-log"><span class="sp-d">Sep 21 14:01:52</span> postfix/smtp[2131]: 4XcB9q0Lw2z1: to=<m.keller@gmx.de>, relay=mx00.gmx.net[212.227.15.9]:25, delay=0.8, status=sent (250 Requested mail action okay)
|
<pre class="sp-log"><span class="sp-ll"><span class="sp-d">Sep 21 14:01:52</span> postfix/smtp[2131]: 4XcB9q0Lw2z1: to=<m.keller@gmx.de>, relay=mx00.gmx.net[212.227.15.9]:25, delay=0.8, status=sent (250 Requested mail action okay)
|
||||||
<span class="sp-d">Sep 21 14:01:52</span> postfix/qmgr[58]: 4XcB9q0Lw2z1: removed
|
</span><span class="sp-ll"><span class="sp-d">Sep 21 14:01:52</span> postfix/qmgr[58]: 4XcB9q0Lw2z1: removed
|
||||||
<span class="sp-d">Sep 21 14:00:18</span> postfix/smtpd[2129]: connect from mail.uni.example[192.0.2.77]
|
</span><span class="sp-ll"><span class="sp-d">Sep 21 14:00:18</span> postfix/smtpd[2129]: connect from mail.uni.example[192.0.2.77]
|
||||||
<span class="sp-d">Sep 21 14:00:18</span> postfix/smtp[2130]: 4XcB8z5Tn1z1: to=<dev@lists.example.org>, relay=mx.internal[10.0.4.2]:25, status=sent (250 2.0.0 Ok: queued)
|
</span><span class="sp-ll"><span class="sp-d">Sep 21 14:00:18</span> postfix/smtp[2130]: 4XcB8z5Tn1z1: to=<dev@lists.example.org>, relay=mx.internal[10.0.4.2]:25, status=sent (250 2.0.0 Ok: queued)
|
||||||
<span class="sp-d">Sep 21 13:58:09</span> <span class="sp-w">postfix/smtp[2123]: 4XcB7k2Jm9z1: to=<noc@example.org>, relay=mx1.example.org[192.0.2.10]:25, status=deferred (451 4.7.1 Greylisted, try again in 5 minutes)</span>
|
</span><span class="sp-ll"><span class="sp-d">Sep 21 13:58:09</span> <span class="sp-w">postfix/smtp[2123]: 4XcB7k2Jm9z1: to=<noc@example.org>, relay=mx1.example.org[192.0.2.10]:25, status=deferred (451 4.7.1 Greylisted, try again in 5 minutes)</span>
|
||||||
<span class="sp-d">Sep 21 13:57:41</span> <span class="sp-e">postfix/smtpd[2119]: NOQUEUE: reject: RCPT from bulk.example[198.51.100.90]: 550 5.1.1 <sales@lists.example.org>: Recipient address rejected: unknown recipient</span>
|
</span><span class="sp-ll"><span class="sp-d">Sep 21 13:57:41</span> <span class="sp-e">postfix/smtpd[2119]: NOQUEUE: reject: RCPT from bulk.example[198.51.100.90]: 550 5.1.1 <sales@lists.example.org>: Recipient address rejected: unknown recipient</span>
|
||||||
<span class="sp-d">Sep 21 13:44:02</span> <span class="sp-w">postfix/smtp[2101]: 4XcB2m7Hd4z1: to=<jobs@acme.io>, relay=none, status=deferred (connect to 10.0.4.12[10.0.4.12]:2525: Connection timed out)</span>
|
</span><span class="sp-ll"><span class="sp-d">Sep 21 13:44:02</span> <span class="sp-w">postfix/smtp[2101]: 4XcB2m7Hd4z1: to=<jobs@acme.io>, relay=none, status=deferred (connect to 10.0.4.12[10.0.4.12]:2525: Connection timed out)</span>
|
||||||
<span class="sp-d">Sep 21 13:41:19</span> <span class="sp-e">postfix/smtp[2098]: 4XcB1c3Rk8z1: to=<j.doe@nonexistent.example>, status=bounced (Host or domain name not found)</span>
|
</span><span class="sp-ll"><span class="sp-d">Sep 21 13:41:19</span> <span class="sp-e">postfix/smtp[2098]: 4XcB1c3Rk8z1: to=<j.doe@nonexistent.example>, status=bounced (Host or domain name not found)</span>
|
||||||
<span class="sp-d">Sep 21 13:40:02</span> <span class="sp-w">journal-milter[40]: rate limit: app=prod-server 201/200 in 3600s — tempfail at MAIL FROM</span></pre>
|
</span><span class="sp-ll"><span class="sp-d">Sep 21 13:40:02</span> <span class="sp-w">journal-milter[40]: rate limit: app=prod-server 201/200 in 3600s — tempfail at MAIL FROM</span>
|
||||||
|
</span></pre>
|
||||||
</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 · Copyright © 2026 Mikhail Yenuchenko · <a href="#">License (AGPL-3.0)</a> · <a href="#">Source</a> · No warranty</footer>
|
||||||
</div>
|
</div>
|
||||||
</main>
|
</main>
|
||||||
</body>
|
</body>
|
||||||
|
|||||||
@@ -73,6 +73,27 @@ code { background: var(--sp-well); color: var(--sp-ink); border-radius: 3px; pad
|
|||||||
.navbar-dropdown button.navbar-item:hover, .navbar-dropdown button.navbar-item:focus { --bulma-navbar-item-background-l-delta: var(--bulma-navbar-item-hover-background-l-delta); }
|
.navbar-dropdown button.navbar-item:hover, .navbar-dropdown button.navbar-item:focus { --bulma-navbar-item-background-l-delta: var(--bulma-navbar-item-hover-background-l-delta); }
|
||||||
.navbar-dropdown button.navbar-item:active { --bulma-navbar-item-background-l-delta: var(--bulma-navbar-item-active-background-l-delta); }
|
.navbar-dropdown button.navbar-item:active { --bulma-navbar-item-background-l-delta: var(--bulma-navbar-item-active-background-l-delta); }
|
||||||
.navbar.is-primary .sp-current > .navbar-link, .navbar.is-primary a.navbar-item.sp-current { background: rgb(0 0 0 / .18); }
|
.navbar.is-primary .sp-current > .navbar-link, .navbar.is-primary a.navbar-item.sp-current { background: rgb(0 0 0 / .18); }
|
||||||
|
/* Below 1024 px Bulma hides the bar's menu (navbar-menu without is-active)
|
||||||
|
and the touch navigation takes its place: Menu at the right of the brand
|
||||||
|
opens a panel over the page, its sections folded but the current one.
|
||||||
|
<details> all through, so it needs no script (owner, 2026-10-16: the menu
|
||||||
|
had been a sheet of every section open, filling the first screen). */
|
||||||
|
.sp-touchnav { display: none; }
|
||||||
|
.sp-touchnav summary { list-style: none; cursor: pointer; }
|
||||||
|
.sp-touchnav summary::-webkit-details-marker { display: none; }
|
||||||
|
.sp-touchnav summary::after { content: "›"; display: inline-block; margin-left: .5rem; font-size: 1.3rem; line-height: 1; transform: rotate(90deg); transition: transform .15s; }
|
||||||
|
.sp-touchnav[open] > summary::after, .sp-touchnav-group[open] > summary::after { transform: rotate(-90deg); }
|
||||||
|
.sp-touchnav > summary { display: flex; align-items: center; padding: 0 1rem; color: var(--sp-on-brick); font-weight: 500; }
|
||||||
|
.sp-touchnav-panel { position: absolute; top: 100%; left: 0; right: 0; z-index: 30; background: var(--sp-brick); box-shadow: 0 12px 28px rgb(18 22 28 / .22); padding-bottom: .4rem; }
|
||||||
|
.sp-touchnav-panel > a, .sp-touchnav-group > summary { display: flex; align-items: center; gap: .45rem; padding: .75rem 1rem; color: var(--sp-on-brick); font-weight: 500; }
|
||||||
|
.sp-touchnav-group > summary::after { margin-left: auto; }
|
||||||
|
.sp-touchnav-panel > a[aria-current], .sp-touchnav-group[open] > summary { background: rgb(0 0 0 / .18); }
|
||||||
|
.sp-touchnav-group a, .sp-touchnav-group button { display: flex; align-items: center; gap: .6rem; padding: .65rem 1rem .65rem 2rem; background: #fff; color: var(--sp-ink); border: 0; font: inherit; text-align: left; cursor: pointer; }
|
||||||
|
.sp-touchnav-group form { display: flex; }
|
||||||
|
.sp-touchnav-group button { flex: 1; }
|
||||||
|
.sp-touchnav-group .ti { color: var(--sp-muted); }
|
||||||
|
.sp-touchnav-group a[aria-current] { color: var(--sp-brick); font-weight: 600; background: var(--sp-brick-tint); }
|
||||||
|
.sp-touchnav-group a[aria-current] .ti { color: var(--sp-brick); }
|
||||||
/* The header is built like the mark: a brick field, then the stamp's paper
|
/* The header is built like the mark: a brick field, then the stamp's paper
|
||||||
margin, and the perforation runs along the outer edge of that margin — round
|
margin, and the perforation runs along the outer edge of that margin — round
|
||||||
holes, flat teeth, the mark's ink hairline (perf-edge.svg is one tooth and
|
holes, flat teeth, the mark's ink hairline (perf-edge.svg is one tooth and
|
||||||
@@ -200,6 +221,11 @@ code { background: var(--sp-well); color: var(--sp-ink); border-radius: 3px; pad
|
|||||||
.sp-facts { display: grid; grid-template-columns: repeat(auto-fit, minmax(11rem, 1fr)); gap: 1rem 1.5rem; margin: 0; }
|
.sp-facts { display: grid; grid-template-columns: repeat(auto-fit, minmax(11rem, 1fr)); gap: 1rem 1.5rem; margin: 0; }
|
||||||
.sp-facts dt { font-family: var(--sp-mono); font-size: var(--sp-fs-xs); letter-spacing: .08em; text-transform: uppercase; color: var(--sp-muted); }
|
.sp-facts dt { font-family: var(--sp-mono); font-size: var(--sp-fs-xs); letter-spacing: .08em; text-transform: uppercase; color: var(--sp-muted); }
|
||||||
.sp-facts dd { margin: 0; overflow-wrap: anywhere; }
|
.sp-facts dd { margin: 0; overflow-wrap: anywhere; }
|
||||||
|
/* A label may carry a tag, the verdict on its value (the certificate's
|
||||||
|
names against the hostname); it keeps the tag's own type. */
|
||||||
|
.sp-facts dt .tag { margin-left: .4rem; text-transform: none; letter-spacing: 0; vertical-align: 1px; }
|
||||||
|
/* A fact that takes the whole row: a line a server wrote, read as one. */
|
||||||
|
.sp-facts .sp-wide { grid-column: 1 / -1; }
|
||||||
.sp-facts dd.sp-big { font-size: 1.5rem; font-weight: 600; line-height: 1.2; }
|
.sp-facts dd.sp-big { font-size: 1.5rem; font-weight: 600; line-height: 1.2; }
|
||||||
.sp-side { position: sticky; top: 1.5rem; }
|
.sp-side { position: sticky; top: 1.5rem; }
|
||||||
.menu-label { font-family: var(--sp-mono); letter-spacing: .1em; }
|
.menu-label { font-family: var(--sp-mono); letter-spacing: .1em; }
|
||||||
@@ -212,6 +238,17 @@ code { background: var(--sp-well); color: var(--sp-ink); border-radius: 3px; pad
|
|||||||
.sp-record-head { display: flex; align-items: center; gap: .6rem; margin-bottom: .75rem; }
|
.sp-record-head { display: flex; align-items: center; gap: .6rem; margin-bottom: .75rem; }
|
||||||
.sp-record-head h3 { font-size: 1rem; font-weight: 600; margin: 0; }
|
.sp-record-head h3 { font-size: 1rem; font-weight: 600; margin: 0; }
|
||||||
.sp-record-head small { margin-left: auto; color: var(--sp-muted); text-align: right; }
|
.sp-record-head small { margin-left: auto; color: var(--sp-muted); text-align: right; }
|
||||||
|
/* A record DNS already holds as shown is folded to its head: a <details>
|
||||||
|
whose summary is the head, opened without script. */
|
||||||
|
details.sp-record > summary { list-style: none; cursor: pointer; }
|
||||||
|
details.sp-record > summary::-webkit-details-marker { display: none; }
|
||||||
|
details.sp-record:not([open]) > summary { margin-bottom: 0; }
|
||||||
|
details.sp-record:not([open]) > summary small { display: none; }
|
||||||
|
details.sp-record[open] .sp-record-head small + .sp-record-toggle { margin-left: 0; }
|
||||||
|
.sp-record-toggle { margin-left: auto; display: inline-flex; align-items: center; gap: .3rem; color: var(--sp-brick); font-size: var(--sp-fs-s); white-space: nowrap; }
|
||||||
|
.sp-record-toggle span + span { display: none; }
|
||||||
|
details.sp-record[open] .sp-record-toggle span { display: none; }
|
||||||
|
details.sp-record[open] .sp-record-toggle span + span { display: inline; }
|
||||||
.sp-record .label, .sp-form .label { font-weight: 500; color: var(--sp-ink-2); margin-bottom: .25rem; }
|
.sp-record .label, .sp-form .label { font-weight: 500; color: var(--sp-ink-2); margin-bottom: .25rem; }
|
||||||
.sp-record .label { font-weight: 400; color: var(--sp-muted); }
|
.sp-record .label { font-weight: 400; color: var(--sp-muted); }
|
||||||
.input[readonly], .textarea[readonly] { font-family: var(--sp-mono); font-size: var(--sp-fs-s); background: var(--sp-well); box-shadow: none; }
|
.input[readonly], .textarea[readonly] { font-family: var(--sp-mono); font-size: var(--sp-fs-s); background: var(--sp-well); box-shadow: none; }
|
||||||
@@ -227,8 +264,12 @@ code { background: var(--sp-well); color: var(--sp-ink); border-radius: 3px; pad
|
|||||||
/* ---- logs, timeline, help */
|
/* ---- logs, timeline, help */
|
||||||
.sp-log {
|
.sp-log {
|
||||||
margin: 0; padding: 1rem 1.25rem; background: #191c21; color: #d9d5cc; border-radius: 0;
|
margin: 0; padding: 1rem 1.25rem; background: #191c21; color: #d9d5cc; border-radius: 0;
|
||||||
font-family: var(--sp-mono); font-size: var(--sp-fs-s); line-height: 1.6; overflow-x: auto; white-space: pre;
|
font-family: var(--sp-mono); font-size: var(--sp-fs-s); line-height: 1.6; white-space: pre-wrap; overflow-wrap: anywhere;
|
||||||
}
|
}
|
||||||
|
/* One line of a log: long ones wrap inside the pane, the rest of the line
|
||||||
|
indented under its start, so nothing scrolls sideways - a phone drew the
|
||||||
|
scroll bars over the text (owner, 2026-10-16). */
|
||||||
|
.sp-log .sp-ll { display: block; padding-left: 4ch; text-indent: -4ch; }
|
||||||
.sp-log .sp-w { color: #e0b15c; } .sp-log .sp-e { color: #ef8f82; } .sp-log .sp-d { color: #7f8894; }
|
.sp-log .sp-w { color: #e0b15c; } .sp-log .sp-e { color: #ef8f82; } .sp-log .sp-d { color: #7f8894; }
|
||||||
.sp-timeline { list-style: none; margin: 0; padding: 0; }
|
.sp-timeline { list-style: none; margin: 0; padding: 0; }
|
||||||
.sp-timeline li { position: relative; padding: 0 0 1.1rem 1.6rem; border-left: 2px solid var(--sp-rule); margin-left: .4rem; }
|
.sp-timeline li { position: relative; padding: 0 0 1.1rem 1.6rem; border-left: 2px solid var(--sp-rule); margin-left: .4rem; }
|
||||||
@@ -262,12 +303,19 @@ code { background: var(--sp-well); color: var(--sp-ink); border-radius: 3px; pad
|
|||||||
.sp-signin-form .sp-foot { text-align: center; margin-top: 2.5rem; }
|
.sp-signin-form .sp-foot { text-align: center; margin-top: 2.5rem; }
|
||||||
|
|
||||||
@media (max-width: 1407px) { .sp-health { grid-template-columns: repeat(3, 1fr); } }
|
@media (max-width: 1407px) { .sp-health { grid-template-columns: repeat(3, 1fr); } }
|
||||||
@media (max-width: 1023px) { .sp-box-head form.sp-end { flex-wrap: wrap; } }
|
@media (max-width: 1023px) {
|
||||||
|
.sp-box-head form.sp-end { flex-wrap: wrap; }
|
||||||
|
.sp-touchnav { display: flex; margin-left: auto; }
|
||||||
|
/* Bulma gives a container side margins only from 1024 px: below it the
|
||||||
|
page met the screen's edge. */
|
||||||
|
.sp-subnav .container, .container.sp-page { padding-inline: 1rem; }
|
||||||
|
}
|
||||||
@media (max-width: 768px) {
|
@media (max-width: 768px) {
|
||||||
.sp-health { grid-template-columns: repeat(2, 1fr); }
|
.sp-health { grid-template-columns: repeat(2, 1fr); }
|
||||||
.sp-row, .sp-pair { grid-template-columns: 1fr; }
|
.sp-row, .sp-pair { grid-template-columns: 1fr; }
|
||||||
.sp-record-head { flex-wrap: wrap; }
|
.sp-record-head { flex-wrap: wrap; }
|
||||||
.sp-record-head small { margin-left: 0; text-align: left; flex-basis: 100%; }
|
.sp-record-head small { margin-left: 0; text-align: left; flex-basis: 100%; order: 1; }
|
||||||
|
details.sp-record[open] .sp-record-head small + .sp-record-toggle { margin-left: auto; }
|
||||||
.sp-postmark { display: none; }
|
.sp-postmark { display: none; }
|
||||||
.sp-cards { display: flex; flex-direction: column; padding-inline: .75rem; }
|
.sp-cards { display: flex; flex-direction: column; padding-inline: .75rem; }
|
||||||
.sp-cards > .column { display: contents; }
|
.sp-cards > .column { display: contents; }
|
||||||
|
|||||||
@@ -12,8 +12,8 @@
|
|||||||
<body>
|
<body>
|
||||||
<nav class="navbar is-primary" aria-label="main navigation">
|
<nav class="navbar is-primary" aria-label="main navigation">
|
||||||
<div class="container">
|
<div class="container">
|
||||||
<div class="navbar-brand"><a class="navbar-item" href="overview.html"><img src="../../selfpost-wordmark.svg" alt="SelfPost"></a></div>
|
<div class="navbar-brand"><a class="navbar-item" href="overview.html"><img src="../../selfpost-wordmark.svg" alt="SelfPost"></a><details class="sp-touchnav"><summary>Menu</summary><div class="sp-touchnav-panel"><a href="overview.html">Overview</a><details class="sp-touchnav-group"><summary>Outbound</summary><a href="out-domains.html"><i class="ti ti-world-upload"></i>Domains</a><a href="out-log.html"><i class="ti ti-list-details"></i>Log</a><a href="out-queue.html"><i class="ti ti-stack-2"></i>Queue</a><a href="dmarc.html"><i class="ti ti-report-analytics"></i>DMARC reports</a></details><details class="sp-touchnav-group"><summary>Inbound</summary><a href="in-domains.html"><i class="ti ti-world-download"></i>Domains</a><a href="in-log.html"><i class="ti ti-list-details"></i>Log</a><a href="in-queue.html"><i class="ti ti-stack-2"></i>Queue</a><a href="in-quarantine.html"><i class="ti ti-mail-pause"></i>Quarantine</a><a href="in-filter-lists.html"><i class="ti ti-filter"></i>Filter lists</a></details><details class="sp-touchnav-group" open><summary>Server</summary><a href="health.html"><i class="ti ti-heartbeat"></i>Health</a><a href="preflight.html"><i class="ti ti-checklist"></i>Preflight</a><a href="system-log.html"><i class="ti ti-file-text"></i>System log</a><a href="backup.html"><i class="ti ti-archive"></i>Backup</a><a href="users.html" aria-current="page"><i class="ti ti-users"></i>Users</a><a href="settings.html"><i class="ti ti-settings"></i>Settings</a></details><details class="sp-touchnav-group"><summary><i class="ti ti-user-circle"></i>admin</summary><a href="account.html"><i class="ti ti-key"></i>Account</a><a href="help.html"><i class="ti ti-help"></i>Help</a><form method="post" action="login.html"><button type="submit"><i class="ti ti-logout"></i>Sign out</button></form></details></div></details></div>
|
||||||
<div class="navbar-menu is-active">
|
<div class="navbar-menu">
|
||||||
<div class="navbar-start">
|
<div class="navbar-start">
|
||||||
<a class="navbar-item" href="overview.html">Overview</a>
|
<a class="navbar-item" href="overview.html">Overview</a>
|
||||||
<div class="navbar-item has-dropdown is-hoverable">
|
<div class="navbar-item has-dropdown is-hoverable">
|
||||||
@@ -80,9 +80,9 @@
|
|||||||
<p class="mb-3">Deleting the panel user <strong>acme-ops</strong> will:</p>
|
<p class="mb-3">Deleting the panel user <strong>acme-ops</strong> will:</p>
|
||||||
<ul class="sp-list-check">
|
<ul class="sp-list-check">
|
||||||
<li><i class="ti ti-x"></i>end any signed-in session of this user at once;</li>
|
<li><i class="ti ti-x"></i>end any signed-in session of this user at once;</li>
|
||||||
<li><i class="ti ti-x"></i>remove the assignments to <strong>all outbound domains</strong> and to <strong>acme.io</strong> (inbound).</li>
|
<li><i class="ti ti-x"></i>remove the assignments to <strong>all outbound domains</strong> (outbound) and to <strong>acme.io</strong> (inbound).</li>
|
||||||
</ul>
|
</ul>
|
||||||
<p class="sp-muted mb-4">The domains, their applications and passwords, upstreams and recipients are not affected.</p>
|
<p class="sp-muted mb-4">Applications and their passwords, upstreams and recipients are not affected.</p>
|
||||||
<form method="post" action="users.html">
|
<form method="post" action="users.html">
|
||||||
<div class="buttons">
|
<div class="buttons">
|
||||||
<button type="submit" class="button is-danger"><i class="ti ti-trash"></i>Delete acme-ops</button>
|
<button type="submit" class="button is-danger"><i class="ti ti-trash"></i>Delete acme-ops</button>
|
||||||
@@ -94,7 +94,7 @@
|
|||||||
</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 · Copyright © 2026 Mikhail Yenuchenko · <a href="#">License (AGPL-3.0)</a> · <a href="#">Source</a> · No warranty</footer>
|
||||||
</div>
|
</div>
|
||||||
</main>
|
</main>
|
||||||
</body>
|
</body>
|
||||||
|
|||||||
@@ -12,8 +12,8 @@
|
|||||||
<body>
|
<body>
|
||||||
<nav class="navbar is-primary" aria-label="main navigation">
|
<nav class="navbar is-primary" aria-label="main navigation">
|
||||||
<div class="container">
|
<div class="container">
|
||||||
<div class="navbar-brand"><a class="navbar-item" href="overview.html"><img src="../../selfpost-wordmark.svg" alt="SelfPost"></a></div>
|
<div class="navbar-brand"><a class="navbar-item" href="overview.html"><img src="../../selfpost-wordmark.svg" alt="SelfPost"></a><details class="sp-touchnav"><summary>Menu</summary><div class="sp-touchnav-panel"><a href="overview.html">Overview</a><details class="sp-touchnav-group"><summary>Outbound</summary><a href="out-domains.html"><i class="ti ti-world-upload"></i>Domains</a><a href="out-log.html"><i class="ti ti-list-details"></i>Log</a><a href="out-queue.html"><i class="ti ti-stack-2"></i>Queue</a><a href="dmarc.html"><i class="ti ti-report-analytics"></i>DMARC reports</a></details><details class="sp-touchnav-group"><summary>Inbound</summary><a href="in-domains.html"><i class="ti ti-world-download"></i>Domains</a><a href="in-log.html"><i class="ti ti-list-details"></i>Log</a><a href="in-queue.html"><i class="ti ti-stack-2"></i>Queue</a><a href="in-quarantine.html"><i class="ti ti-mail-pause"></i>Quarantine</a><a href="in-filter-lists.html"><i class="ti ti-filter"></i>Filter lists</a></details><details class="sp-touchnav-group" open><summary>Server</summary><a href="health.html"><i class="ti ti-heartbeat"></i>Health</a><a href="preflight.html"><i class="ti ti-checklist"></i>Preflight</a><a href="system-log.html"><i class="ti ti-file-text"></i>System log</a><a href="backup.html"><i class="ti ti-archive"></i>Backup</a><a href="users.html" aria-current="page"><i class="ti ti-users"></i>Users</a><a href="settings.html"><i class="ti ti-settings"></i>Settings</a></details><details class="sp-touchnav-group"><summary><i class="ti ti-user-circle"></i>admin</summary><a href="account.html"><i class="ti ti-key"></i>Account</a><a href="help.html"><i class="ti ti-help"></i>Help</a><form method="post" action="login.html"><button type="submit"><i class="ti ti-logout"></i>Sign out</button></form></details></div></details></div>
|
||||||
<div class="navbar-menu is-active">
|
<div class="navbar-menu">
|
||||||
<div class="navbar-start">
|
<div class="navbar-start">
|
||||||
<a class="navbar-item" href="overview.html">Overview</a>
|
<a class="navbar-item" href="overview.html">Overview</a>
|
||||||
<div class="navbar-item has-dropdown is-hoverable">
|
<div class="navbar-item has-dropdown is-hoverable">
|
||||||
@@ -69,7 +69,6 @@
|
|||||||
<div class="sp-grow">
|
<div class="sp-grow">
|
||||||
<p class="sp-kicker"><a href="users.html">Users</a> /</p>
|
<p class="sp-kicker"><a href="users.html">Users</a> /</p>
|
||||||
<h1 class="title is-3">acme-ops</h1>
|
<h1 class="title is-3">acme-ops</h1>
|
||||||
<p class="sp-lead">The same form, empty, creates a new user.</p>
|
|
||||||
</div>
|
</div>
|
||||||
<a class="button is-danger is-light" href="user-delete.html"><i class="ti ti-trash"></i>Delete user</a>
|
<a class="button is-danger is-light" href="user-delete.html"><i class="ti ti-trash"></i>Delete user</a>
|
||||||
</div>
|
</div>
|
||||||
@@ -81,8 +80,8 @@
|
|||||||
<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-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 <span class="tag is-success is-light">confirmed</span></label><div class="control"><input class="input" id="email" name="email" type="email" value="ops@acme.io" autocomplete="off"></div>
|
||||||
<p class="help">For notifications and a password-reset link. The user can change it under Account.</p></div>
|
<p class="help">The user's own address. They can change it under Account. A new one is sent a link to confirm it; until it is confirmed no reset link goes to it.</p></div>
|
||||||
<div class="field"><label class="label" for="password">Password</label><div class="control"><input class="input" id="password" name="password" type="password" autocomplete="new-password"></div>
|
<div class="field"><label class="label" for="password">Password</label><div class="control"><input class="input" id="password" name="password" type="password" autocomplete="new-password"></div>
|
||||||
<p class="help">Leave empty to keep the current one.</p></div>
|
<p class="help">Leave empty to keep the current one.</p></div>
|
||||||
<div class="field"><label class="label" for="role">Role</label>
|
<div class="field"><label class="label" for="role">Role</label>
|
||||||
@@ -99,7 +98,7 @@
|
|||||||
<div class="field"><label class="checkbox"><input type="checkbox" name="domain_ids" checked disabled> example.org</label></div>
|
<div class="field"><label class="checkbox"><input type="checkbox" name="domain_ids" checked disabled> example.org</label></div>
|
||||||
<div class="field"><label class="checkbox"><input type="checkbox" name="domain_ids" checked disabled> shop.example.org</label></div>
|
<div class="field"><label class="checkbox"><input type="checkbox" name="domain_ids" checked disabled> shop.example.org</label></div>
|
||||||
<div class="field"><label class="checkbox"><input type="checkbox" name="domain_ids" checked disabled> notify.acme.io</label></div>
|
<div class="field"><label class="checkbox"><input type="checkbox" name="domain_ids" checked disabled> notify.acme.io</label></div>
|
||||||
<p class="help">With <em>All</em> ticked the list below it is ignored — and greyed out where scripts run.</p>
|
<p class="help">With <em>All</em> ticked the single domains are ignored.</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="box">
|
<div class="box">
|
||||||
@@ -119,7 +118,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
<footer class="sp-foot">SelfPost 1.8.0 · © 2026 Mix · <a href="#">License (AGPL-3.0)</a> · <a href="#">Source</a> · No warranty</footer>
|
<footer class="sp-foot">SelfPost 1.8.0 · Copyright © 2026 Mikhail Yenuchenko · <a href="#">License (AGPL-3.0)</a> · <a href="#">Source</a> · No warranty</footer>
|
||||||
</div>
|
</div>
|
||||||
</main>
|
</main>
|
||||||
</body>
|
</body>
|
||||||
|
|||||||
@@ -12,8 +12,8 @@
|
|||||||
<body>
|
<body>
|
||||||
<nav class="navbar is-primary" aria-label="main navigation">
|
<nav class="navbar is-primary" aria-label="main navigation">
|
||||||
<div class="container">
|
<div class="container">
|
||||||
<div class="navbar-brand"><a class="navbar-item" href="overview.html"><img src="../../selfpost-wordmark.svg" alt="SelfPost"></a></div>
|
<div class="navbar-brand"><a class="navbar-item" href="overview.html"><img src="../../selfpost-wordmark.svg" alt="SelfPost"></a><details class="sp-touchnav"><summary>Menu</summary><div class="sp-touchnav-panel"><a href="overview.html">Overview</a><details class="sp-touchnav-group"><summary>Outbound</summary><a href="out-domains.html"><i class="ti ti-world-upload"></i>Domains</a><a href="out-log.html"><i class="ti ti-list-details"></i>Log</a><a href="out-queue.html"><i class="ti ti-stack-2"></i>Queue</a><a href="dmarc.html"><i class="ti ti-report-analytics"></i>DMARC reports</a></details><details class="sp-touchnav-group"><summary>Inbound</summary><a href="in-domains.html"><i class="ti ti-world-download"></i>Domains</a><a href="in-log.html"><i class="ti ti-list-details"></i>Log</a><a href="in-queue.html"><i class="ti ti-stack-2"></i>Queue</a><a href="in-quarantine.html"><i class="ti ti-mail-pause"></i>Quarantine</a><a href="in-filter-lists.html"><i class="ti ti-filter"></i>Filter lists</a></details><details class="sp-touchnav-group" open><summary>Server</summary><a href="health.html"><i class="ti ti-heartbeat"></i>Health</a><a href="preflight.html"><i class="ti ti-checklist"></i>Preflight</a><a href="system-log.html"><i class="ti ti-file-text"></i>System log</a><a href="backup.html"><i class="ti ti-archive"></i>Backup</a><a href="users.html" aria-current="page"><i class="ti ti-users"></i>Users</a><a href="settings.html"><i class="ti ti-settings"></i>Settings</a></details><details class="sp-touchnav-group"><summary><i class="ti ti-user-circle"></i>admin</summary><a href="account.html"><i class="ti ti-key"></i>Account</a><a href="help.html"><i class="ti ti-help"></i>Help</a><form method="post" action="login.html"><button type="submit"><i class="ti ti-logout"></i>Sign out</button></form></details></div></details></div>
|
||||||
<div class="navbar-menu is-active">
|
<div class="navbar-menu">
|
||||||
<div class="navbar-start">
|
<div class="navbar-start">
|
||||||
<a class="navbar-item" href="overview.html">Overview</a>
|
<a class="navbar-item" href="overview.html">Overview</a>
|
||||||
<div class="navbar-item has-dropdown is-hoverable">
|
<div class="navbar-item has-dropdown is-hoverable">
|
||||||
@@ -88,7 +88,7 @@
|
|||||||
</table></div>
|
</table></div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<footer class="sp-foot">SelfPost 1.8.0 · © 2026 Mix · <a href="#">License (AGPL-3.0)</a> · <a href="#">Source</a> · No warranty</footer>
|
<footer class="sp-foot">SelfPost 1.8.0 · Copyright © 2026 Mikhail Yenuchenko · <a href="#">License (AGPL-3.0)</a> · <a href="#">Source</a> · No warranty</footer>
|
||||||
</div>
|
</div>
|
||||||
</main>
|
</main>
|
||||||
</body>
|
</body>
|
||||||
|
|||||||
@@ -46,6 +46,14 @@ the trixie sources. This register is left as it was made: its line numbers
|
|||||||
and versions are those of the bookworm sources above, which are now kept
|
and versions are those of the bookworm sources above, which are now kept
|
||||||
under `dev/upstream-bookworm/` (where it says `dev/upstream/`).
|
under `dev/upstream-bookworm/` (where it says `dev/upstream/`).
|
||||||
|
|
||||||
|
With panel-dkim-signing (`2.8.0`) OpenDKIM left the image: the panel signs
|
||||||
|
outgoing mail itself (`internal/milter/sign.go`), and `build/opendkim.conf`,
|
||||||
|
the syslog relay (`internal/syslogrelay`, `/dev/log`), the KeyTable and
|
||||||
|
SigningTable, the USR1 reload through supervisord and the port of
|
||||||
|
OpenDKIM's From: parser (`internal/milter/mailparse.go`) went with it. The
|
||||||
|
rows about OpenDKIM, the syslog relay and those pieces describe code that
|
||||||
|
is gone; they are left as they were, like the rest of this register.
|
||||||
|
|
||||||
Counts at the inventory, by the justification the tree had:
|
Counts at the inventory, by the justification the tree had:
|
||||||
|
|
||||||
| Part | Entries | CITED | MEASURED | GUESSED | WRONG? |
|
| Part | Entries | CITED | MEASURED | GUESSED | WRONG? |
|
||||||
|
|||||||
+68
-26
@@ -107,7 +107,7 @@ than the default «meaningful step» rule in [§ Commits and release build](#com
|
|||||||
2. Git tag `vX.Y.Z` and publish the GitHub Release only on explicit request (see
|
2. Git tag `vX.Y.Z` and publish the GitHub Release only on explicit request (see
|
||||||
[§ Release image](#release-image)).
|
[§ Release image](#release-image)).
|
||||||
|
|
||||||
**Planned version cuts** (from pin `2.7.0`; adjust if semver changes mid-track):
|
**Planned version cuts** (from pin `2.14.2`; adjust if semver changes mid-track):
|
||||||
|
|
||||||
| Stage | ID | Cut |
|
| Stage | ID | Cut |
|
||||||
|---|---|---|
|
|---|---|---|
|
||||||
@@ -140,6 +140,20 @@ than the default «meaningful step» rule in [§ Commits and release build](#com
|
|||||||
| 27 | port25-bare-newline | `2.6.1` (PATCH) |
|
| 27 | port25-bare-newline | `2.6.1` (PATCH) |
|
||||||
| 28 | base-trixie | `2.7.0` |
|
| 28 | base-trixie | `2.7.0` |
|
||||||
| 29 | panel-dkim-signing | `2.8.0` |
|
| 29 | panel-dkim-signing | `2.8.0` |
|
||||||
|
| 30 | dns-record-collapse, settings-save-row, notification-mail-recheck; `/data/opendkim` removed | `2.9.0` |
|
||||||
|
| 31 | log panes wrap on a phone | `2.9.1` (PATCH) |
|
||||||
|
| 32 | inbound-upstream-result | `2.10.0` |
|
||||||
|
| 33 | long, non-repeating queue ids | `2.10.1` (PATCH) |
|
||||||
|
| 34 | phone-desktop-mode | `2.10.2` (PATCH) |
|
||||||
|
| 35 | backup-reauth | `2.10.3` (PATCH) |
|
||||||
|
| 36 | origin-strict | `2.10.4` (PATCH) |
|
||||||
|
| 37 | health-setup-checks | `2.11.0` |
|
||||||
|
| 38 | test-email | `2.12.0` |
|
||||||
|
| 39 | local-time | `2.13.0` |
|
||||||
|
| 40 | docs-audit | `2.13.1` |
|
||||||
|
| 41 | password-reset | `2.14.0` |
|
||||||
|
| 42 | overview-head | `2.14.1` |
|
||||||
|
| 43 | dmarc-day, health-sockets | `2.14.2` |
|
||||||
|
|
||||||
Docs-only prep (checklists in plans, no product code) uses the same per-step
|
Docs-only prep (checklists in plans, no product code) uses the same per-step
|
||||||
commit rule but **no** version cut until the next product stage ships.
|
commit rule but **no** version cut until the next product stage ships.
|
||||||
@@ -155,16 +169,16 @@ commit rule but **no** version cut until the next product stage ships.
|
|||||||
| **Build** | [Makefile](../Makefile): primary targets `vet`, `test`, `build`, `e2e` (also `all`, `clean`) |
|
| **Build** | [Makefile](../Makefile): primary targets `vet`, `test`, `build`, `e2e` (also `all`, `clean`) |
|
||||||
| **Container** | Docker + Compose v2 on the dev host and in CI |
|
| **Container** | Docker + Compose v2 on the dev host and in CI |
|
||||||
| **Image (build stage)** | `golang:1.26-trixie` (bookworm until `2.7.0`) — [build/Dockerfile](../build/Dockerfile). The tag floats to the newest 1.26 patch release, and most standard-library security fixes (`net/http`, `html/template`, …) arrive only that way: rebuild the image after each Go patch release, no edit needed. Dependencies are raised by hand (`go get …@latest`, `go mod tidy`), at least at each audit |
|
| **Image (build stage)** | `golang:1.26-trixie` (bookworm until `2.7.0`) — [build/Dockerfile](../build/Dockerfile). The tag floats to the newest 1.26 patch release, and most standard-library security fixes (`net/http`, `html/template`, …) arrive only that way: rebuild the image after each Go patch release, no edit needed. Dependencies are raised by hand (`go get …@latest`, `go mod tidy`), at least at each audit |
|
||||||
| **Image (runtime)** | `debian:trixie-slim` (bookworm until `2.7.0`) + Postfix, OpenDKIM, supervisord, SASL, logrotate |
|
| **Image (runtime)** | `debian:trixie-slim` (bookworm until `2.7.0`) + Postfix, supervisord, SASL, logrotate (OpenDKIM until `2.8.0`; the panel signs since) |
|
||||||
| **CI** | GitHub Actions — [.github/workflows/](../.github/workflows/) |
|
| **CI** | GitHub Actions — [.github/workflows/](../.github/workflows/) |
|
||||||
| **Image registry** | `ghcr.io/mixeme/selfpost` |
|
| **Image registry** | `ghcr.io/mixeme/selfpost` |
|
||||||
|
|
||||||
**Repository layout** (brief; process details in [architecture.md](architecture.md)):
|
**Repository layout** (brief; process details in [architecture.md](architecture.md)):
|
||||||
|
|
||||||
- `cmd/panel` — HTTP panel + journal-milter + log-tailer
|
- `cmd/panel` — HTTP panel + milters (sender check, DKIM signer, journal) + log-tailer + notifier
|
||||||
- `cmd/selfpost-backup` — backup CLI (`docker exec … selfpost-backup`)
|
- `cmd/selfpost-backup` — backup CLI (`docker exec … selfpost-backup`)
|
||||||
- `internal/` — domain logic, store, web, health
|
- `internal/` — domain logic, store, web, health
|
||||||
- `build/` — Dockerfile, supervisord, Postfix/OpenDKIM, entrypoint
|
- `build/` — Dockerfile, supervisord, Postfix, entrypoint
|
||||||
- `deploy/` — `docker-compose.yml`, proxy examples, `.env.example`
|
- `deploy/` — `docker-compose.yml`, proxy examples, `.env.example`
|
||||||
- `test/e2e/` — **separate Go module**; container integration tests
|
- `test/e2e/` — **separate Go module**; container integration tests
|
||||||
|
|
||||||
@@ -182,9 +196,10 @@ dependencies must be permissive or GPL-family (see
|
|||||||
|
|
||||||
| Package | Version | Repository | License |
|
| Package | Version | Repository | License |
|
||||||
|---|---|---|---|
|
|---|---|---|---|
|
||||||
| `github.com/emersion/go-milter` | v0.4.1 | <https://github.com/emersion/go-milter> | BSD-2-Clause |
|
| `github.com/emersion/go-milter` | v0.4.1, patched in `third_party/go-milter` (its `PATCHES.md`; `replace` in `go.mod`) | <https://github.com/emersion/go-milter> | BSD-2-Clause |
|
||||||
| `golang.org/x/crypto` | v0.54.0 | <https://github.com/golang/crypto> | BSD-3-Clause |
|
| `github.com/emersion/go-msgauth` | v0.7.0 (`dkim`, the signer) | <https://github.com/emersion/go-msgauth> | MIT |
|
||||||
| `modernc.org/sqlite` | v1.53.0 | <https://gitlab.com/cznic/sqlite> (mirror: <https://github.com/modernc-org/sqlite>) | BSD-3-Clause |
|
| `golang.org/x/crypto` | v0.58.0 | <https://github.com/golang/crypto> | BSD-3-Clause |
|
||||||
|
| `modernc.org/sqlite` | v1.60.1 | <https://gitlab.com/cznic/sqlite> (mirror: <https://github.com/modernc-org/sqlite>) | BSD-3-Clause |
|
||||||
|
|
||||||
Transitive dependencies — `go mod graph` / `go.sum`; all indirect packages in
|
Transitive dependencies — `go mod graph` / `go.sum`; all indirect packages in
|
||||||
the tree are AGPL-3.0-compatible.
|
the tree are AGPL-3.0-compatible.
|
||||||
@@ -203,14 +218,16 @@ the tree are AGPL-3.0-compatible.
|
|||||||
|
|
||||||
| Package | Version | Repository | License |
|
| Package | Version | Repository | License |
|
||||||
|---|---|---|---|
|
|---|---|---|---|
|
||||||
| `github.com/emersion/go-msgauth` | v0.6.8 | <https://github.com/emersion/go-msgauth> | BSD-2-Clause |
|
| `github.com/emersion/go-msgauth` | v0.6.8 | <https://github.com/emersion/go-msgauth> | MIT |
|
||||||
|
|
||||||
The test module is not part of the main `go build` graph and is not shipped in
|
The test module is not part of the main `go build` graph and is not shipped in
|
||||||
the image.
|
the image. The e2e sink image also installs dkimpy (`python3-dkim`, Debian
|
||||||
|
trixie) as the verifier that is not the signer's library
|
||||||
|
([test/e2e/sink/Dockerfile](../test/e2e/sink/Dockerfile)).
|
||||||
|
|
||||||
### Debian packages in the runtime image
|
### Debian packages in the runtime image
|
||||||
|
|
||||||
Postfix, OpenDKIM, `supervisord`, `sasl2-bin`, `logrotate`, and others come
|
Postfix, `supervisord`, `sasl2-bin`, `logrotate`, and others come
|
||||||
from Debian trixie repositories; licenses are in each package's `copyright`
|
from Debian trixie repositories; licenses are in each package's `copyright`
|
||||||
file on <https://packages.debian.org/trixie/>.
|
file on <https://packages.debian.org/trixie/>.
|
||||||
The image also ships [LICENSE](../LICENSE), [NOTICE](../NOTICE), and the IBM
|
The image also ships [LICENSE](../LICENSE), [NOTICE](../NOTICE), and the IBM
|
||||||
@@ -222,9 +239,9 @@ the OFL text at `/static/OFL.txt`.
|
|||||||
|
|
||||||
The owner's rule (2026-10-14): what another program does is read in its
|
The owner's rule (2026-10-14): what another program does is read in its
|
||||||
documentation or its source - never guessed. SelfPost reads Postfix's log
|
documentation or its source - never guessed. SelfPost reads Postfix's log
|
||||||
lines and reply codes, feeds OpenDKIM's tables, parses `postqueue`,
|
lines and reply codes, parses `postqueue`, `postconf`, `supervisorctl` and
|
||||||
`postconf`, `supervisorctl` and `db_dump` output, talks the milter protocol
|
`db_dump` output, talks the milter protocol through go-milter, signs with
|
||||||
through go-milter, relies on SQLite's locking, on logrotate's and Docker's
|
go-msgauth over the headers Postfix hands a milter, relies on SQLite's locking, on logrotate's and Docker's
|
||||||
behaviour. Every block of code that does so carries a comment that says
|
behaviour. Every block of code that does so carries a comment that says
|
||||||
what it relies on and where that is written, for the version the image runs
|
what it relies on and where that is written, for the version the image runs
|
||||||
(the Debian trixie package, not upstream's head):
|
(the Debian trixie package, not upstream's head):
|
||||||
@@ -236,11 +253,14 @@ what it relies on and where that is written, for the version the image runs
|
|||||||
```
|
```
|
||||||
|
|
||||||
The Debian sources of the image's versions are fetched with `apt-get source`
|
The Debian sources of the image's versions are fetched with `apt-get source`
|
||||||
inside a `debian:trixie` container (`postfix`, `opendkim`, `cyrus-sasl2`,
|
inside a `debian:trixie` container (`postfix`, `cyrus-sasl2`, `supervisor`,
|
||||||
`supervisor`, `logrotate`, and `sendmail` for libmilter); `dev/upstream/`
|
`logrotate`); `dev/upstream/` (not in git) is where they are kept for
|
||||||
(not in git) is where they are kept for reading, and `dev/upstream-bookworm/`
|
reading, and `dev/upstream-bookworm/` holds the bookworm ones the citations
|
||||||
holds the bookworm ones the citations named before `2.7.0`. Go modules are
|
named before `2.7.0`. `opendkim` and `sendmail` (for libmilter) are no longer
|
||||||
read in the module cache at the version `go.mod` pins.
|
the image's since `2.8.0`; they stay in `dev/upstream/` only as what the
|
||||||
|
removed code and some historical comments cite (the signer's header list,
|
||||||
|
`internal/milter/sign.go`, names OpenDKIM's). Go modules are read in the
|
||||||
|
module cache at the version `go.mod` pins.
|
||||||
|
|
||||||
The image takes `debian:trixie-slim` and installs without pinning (pinned
|
The image takes `debian:trixie-slim` and installs without pinning (pinned
|
||||||
Debian versions leave the mirrors), so a rebuild after a point or security
|
Debian versions leave the mirrors), so a rebuild after a point or security
|
||||||
@@ -467,7 +487,8 @@ make e2e
|
|||||||
**Stack:** [deploy/docker-compose.yml](../deploy/docker-compose.yml) +
|
**Stack:** [deploy/docker-compose.yml](../deploy/docker-compose.yml) +
|
||||||
[test/e2e/compose.override.yml](../test/e2e/compose.override.yml) — same
|
[test/e2e/compose.override.yml](../test/e2e/compose.override.yml) — same
|
||||||
`cap_drop`/`cap_add`/`no-new-privileges` as production. Override: high ports
|
`cap_drop`/`cap_add`/`no-new-privileges` as production. Override: high ports
|
||||||
(`20465`/`20587`/`20080`), test hostname, self-signed TLS,
|
(`20465`/`20587`/`20080`, port 25 at `20025`, the stand's CoreDNS at
|
||||||
|
`20053/udp`), test hostname, self-signed TLS,
|
||||||
`PANEL_COOKIE_SECURE=false`, isolated compose project, and the image's
|
`PANEL_COOKIE_SECURE=false`, isolated compose project, and the image's
|
||||||
`VERSION` from `SELFPOST_E2E_VERSION` (unset locally: `e2e`). Mail is hermetic:
|
`VERSION` from `SELFPOST_E2E_VERSION` (unset locally: `e2e`). Mail is hermetic:
|
||||||
CoreDNS (fake zone) + Postfix `smtp-sink` as sink-MX; DKIM TXT is scraped from
|
CoreDNS (fake zone) + Postfix `smtp-sink` as sink-MX; DKIM TXT is scraped from
|
||||||
@@ -480,9 +501,30 @@ L1/L2 limits, the panel down or hung mid-message (Postfix defers the mail
|
|||||||
rather than letting it through: `panel_down_defers_mail`,
|
rather than letting it through: `panel_down_defers_mail`,
|
||||||
`panel_hung_mid_message_is_counted`), bad `SELFPOST_HOSTNAME`, session
|
`panel_hung_mid_message_is_counted`), bad `SELFPOST_HOSTNAME`, session
|
||||||
survives `docker restart`); startup checks that supervisord actually brought up
|
survives `docker restart`); startup checks that supervisord actually brought up
|
||||||
OpenDKIM, the panel, and Postfix (`checkSupervisorProcesses`), plus logrotate
|
the panel, Postfix, cert-reload and logrotate (`checkSupervisorProcesses`),
|
||||||
config-mode and forced-rotation checks (`checkLogrotateConfigMode`,
|
that `/data/dkim` is the panel's alone, directories 0700 and files 0600
|
||||||
`checkLogrotateRotation` — [test/e2e/logrotate_check.go](../test/e2e/logrotate_check.go)).
|
(`checkDKIMTree`), and that the image's Debian packages are the cited ones
|
||||||
|
(`checkCitedPackageVersions`), plus logrotate config-mode and forced-rotation
|
||||||
|
checks (`checkLogrotateConfigMode`, `checkLogrotateRotation` —
|
||||||
|
[test/e2e/logrotate_check.go](../test/e2e/logrotate_check.go)).
|
||||||
|
|
||||||
|
DKIM has an oracle that is not the signer's library: on every positive send
|
||||||
|
and on a notification, `checkPanelSignature`
|
||||||
|
([test/e2e/mail_helpers.go](../test/e2e/mail_helpers.go)) requires exactly
|
||||||
|
one signature, the panel's (`relaxed/simple`, `rsa-sha256`, `From` signed and
|
||||||
|
over-signed), and has dkimpy (`dkimverify` in the sink) verify it against the
|
||||||
|
stand's CoreDNS; go-msgauth's verifier (`verifyDKIM`) checks the published
|
||||||
|
record as a receiver would. `signature_over_awkward_headers`
|
||||||
|
([test/e2e/dkim_shapes_test.go](../test/e2e/dkim_shapes_test.go)) sends the
|
||||||
|
header shapes Postfix hands a milter in its own way - folded, tab-led, no
|
||||||
|
space after the colon, two `To:` - and an 8-bit body, and dkimpy verifies
|
||||||
|
what was delivered. `TestStartsOnA28Data`
|
||||||
|
([test/e2e/upgrade_test.go](../test/e2e/upgrade_test.go)) starts the image on
|
||||||
|
a `/data` a 2.8.x image left: the old key tree `/data/opendkim` is removed,
|
||||||
|
the key under `/data/dkim` is kept unchanged and the selector table names
|
||||||
|
it, and a queue owned by other uids is handed to Postfix's users by name;
|
||||||
|
`TestRefusesA27Data` has the start stop on a `/data` whose keys are under
|
||||||
|
the old tree only, which is left as it was.
|
||||||
Waits poll with a timeout (`waitFor`); a fixed sleep is the exception and says
|
Waits poll with a timeout (`waitFor`); a fixed sleep is the exception and says
|
||||||
why where it stands (`sender_check_test.go`: a downtime worth a second or two,
|
why where it stands (`sender_check_test.go`: a downtime worth a second or two,
|
||||||
and 35 s to show a row is not written again).
|
and 35 s to show a row is not written again).
|
||||||
@@ -499,10 +541,10 @@ Workflows in [.github/workflows/](../.github/workflows/). What each job runs —
|
|||||||
### `test.yml` — every push and PR to `main`
|
### `test.yml` — every push and PR to `main`
|
||||||
|
|
||||||
`design-first` (needs the full history) → `gofmt -l` → `go vet ./...` →
|
`design-first` (needs the full history) → `gofmt -l` → `go vet ./...` →
|
||||||
`go test ./...` (main module, no e2e; includes the panel guard tests, see
|
Staticcheck 2026.1 → `go test ./...` (main module, no e2e; includes the panel guard tests, see
|
||||||
[§ Panel pages](#panel-pages-the-component-kit-and-its-guards)) → the e2e
|
[§ Panel pages](#panel-pages-the-component-kit-and-its-guards)) → the e2e
|
||||||
module compiled, not run (`go vet ./...` and `go test -c -o /dev/null .` in
|
module compiled, not run (`go vet ./...`, Staticcheck and
|
||||||
`test/e2e`).
|
`go test -c -o /dev/null .` in `test/e2e`).
|
||||||
|
|
||||||
### `release.yml` — published GitHub Release, or `workflow_dispatch` with SemVer
|
### `release.yml` — published GitHub Release, or `workflow_dispatch` with SemVer
|
||||||
|
|
||||||
@@ -528,7 +570,7 @@ prepare (version from release tag or workflow_dispatch input; checkout vX.Y.Z)
|
|||||||
→ GitHub Packages API → drop untagged versions no tagged manifest references
|
→ GitHub Packages API → drop untagged versions no tagged manifest references
|
||||||
```
|
```
|
||||||
|
|
||||||
Native per-arch matrix (no QEMU): running the full Postfix/OpenDKIM stack under
|
Native per-arch matrix (no QEMU): running the full Postfix stack under
|
||||||
emulation for e2e is impractical. E2e first, then push — the registry receives
|
emulation for e2e is impractical. E2e first, then push — the registry receives
|
||||||
the bytes that passed the gate. Each release leaves three tags in GHCR:
|
the bytes that passed the gate. Each release leaves three tags in GHCR:
|
||||||
`X.Y.Z` (the multi-arch manifest operators pin) and `X.Y.Z-amd64` /
|
`X.Y.Z` (the multi-arch manifest operators pin) and `X.Y.Z-amd64` /
|
||||||
|
|||||||
+273
-71
@@ -84,8 +84,9 @@ Since `2.7.0` a message sent on 465 or 587 does not tell its recipients
|
|||||||
where the application runs: the `Received:` header SelfPost adds reads
|
where the application runs: the `Received:` header SelfPost adds reads
|
||||||
`Received: by mail.example.com (Postfix) id 4AbC12xyz for <...>; <date>`,
|
`Received: by mail.example.com (Postfix) id 4AbC12xyz for <...>; <date>`,
|
||||||
without the application's address, host name or login. To trace a message
|
without the application's address, host name or login. To trace a message
|
||||||
someone complains about, take that queue id to *Outbound › Log*; mail.log
|
someone complains about, find its row in *Outbound › Log* (by domain,
|
||||||
keeps the client address on smtpd's `client=` line for the same id.
|
application and day; the message's page shows its queue id); mail.log keeps
|
||||||
|
the client address on smtpd's `client=` line for the same id.
|
||||||
|
|
||||||
### Local trial
|
### Local trial
|
||||||
|
|
||||||
@@ -204,14 +205,14 @@ cat ./data/setup-token
|
|||||||
#### Fixed image tag
|
#### Fixed image tag
|
||||||
|
|
||||||
`deploy/docker-compose.yml` pins an explicit version (`ghcr.io/mixeme/selfpost:X.Y.Z`),
|
`deploy/docker-compose.yml` pins an explicit version (`ghcr.io/mixeme/selfpost:X.Y.Z`),
|
||||||
deliberately never `:latest`. The current pin is `2.7.0`. **No 2.x image is
|
deliberately never `:latest`. The current pin is `2.14.2`. **No 2.x image is
|
||||||
published on ghcr.io yet** (the newest there is `1.3.0`); until one is, build
|
published on ghcr.io yet** (the newest there is `1.3.0`); until one is, build
|
||||||
it from the checkout under the pinned name, and Compose uses the local image
|
it from the checkout under the pinned name, and Compose uses the local image
|
||||||
instead of pulling:
|
instead of pulling:
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
docker build -f build/Dockerfile --build-arg VERSION=2.7.0 \
|
docker build -f build/Dockerfile --build-arg VERSION=2.14.2 \
|
||||||
-t ghcr.io/mixeme/selfpost:2.7.0 .
|
-t ghcr.io/mixeme/selfpost:2.14.2 .
|
||||||
```
|
```
|
||||||
|
|
||||||
`VERSION` must equal the tag: it is the version the backup check compares
|
`VERSION` must equal the tag: it is the version the backup check compares
|
||||||
@@ -249,7 +250,13 @@ expected to set; defaults match the code exactly.
|
|||||||
| `SEND_LOG_RETENTION_DAYS` | Initial default for how many days of send-log history are kept before the background sweep deletes rows, and with them the `mail.log` lines kept for each message (1–2 KB a message) — the main driver of `/data` growth over time and of the size of a full backup. After the first panel start, change retention under **Server › Settings** (global role); the env value is only used to seed SQLite when the setting has never been saved. | `90` | `.env` |
|
| `SEND_LOG_RETENTION_DAYS` | Initial default for how many days of send-log history are kept before the background sweep deletes rows, and with them the `mail.log` lines kept for each message (1–2 KB a message) — the main driver of `/data` growth over time and of the size of a full backup. After the first panel start, change retention under **Server › Settings** (global role); the env value is only used to seed SQLite when the setting has never been saved. | `90` | `.env` |
|
||||||
| `PANEL_SESSION_IDLE_DAYS` | Sliding idle timeout for the panel login session, in days. There is no absolute cap: a user who keeps coming back stays signed in indefinitely. | `7` | `.env` |
|
| `PANEL_SESSION_IDLE_DAYS` | Sliding idle timeout for the panel login session, in days. There is no absolute cap: a user who keeps coming back stays signed in indefinitely. | `7` | `.env` |
|
||||||
| `SELFPOST_DNS_RESOLVERS` | Comma-separated recursive resolvers the panel's PTR/SPF/DKIM/DMARC checks query directly (so they report what the internet sees, not what this host's stub resolver synthesises). Each is given 2 seconds to answer before the next is asked, and the one that answered last is asked first; a lookup none of them answered shows as not known, never as a wrong record. | `1.1.1.1:53`, `8.8.8.8:53`, `9.9.9.9:53` when unset | `.env` |
|
| `SELFPOST_DNS_RESOLVERS` | Comma-separated recursive resolvers the panel's PTR/SPF/DKIM/DMARC checks query directly (so they report what the internet sees, not what this host's stub resolver synthesises). Each is given 2 seconds to answer before the next is asked, and the one that answered last is asked first; a lookup none of them answered shows as not known, never as a wrong record. | `1.1.1.1:53`, `8.8.8.8:53`, `9.9.9.9:53` when unset | `.env` |
|
||||||
| `TRUSTED_PROXY_CIDR` | Comma-separated CIDRs (bare IPs allowed) of reverse proxies allowed to supply `X-Forwarded-For` for login, setup, and account-change rate-limiting. **Leave unset unless you know the exact address of your reverse proxy.** A wrong value lets a client spoof its rate-limit key by sending a forged `X-Forwarded-For` header — the panel trusts the last hop only when the TCP peer matches one of these CIDRs. With Apache on the host (the default), the panel sees the proxy's connections come from the Docker network's gateway, e.g. `172.18.0.1` (`docker network inspect` shows it; measured 2026-10-15: the panel's `127.0.0.1:8080` is published through Docker's proxy, and a connection to such a port from the host arrives from the gateway). | *(empty — XFF ignored)* | `.env` |
|
| `TZ` | Time zone of the container, an IANA name such as `Europe/Moscow`: every time the panel shows, every day it counts (today on Health, the days of the statistics, the days of the log filters) and the stamps Postfix writes in `mail.log`, so the two agree. Stored times stay UTC, so changing it is only a restart. *Server › Settings* shows the zone in use, and says so when the image does not know the name (the times are then UTC). | UTC | `.env` |
|
||||||
|
| `TRUSTED_PROXY_CIDR` | Comma-separated CIDRs (bare IPs allowed) of reverse proxies allowed to supply `X-Forwarded-For` for sign-in and setup rate-limiting (account-change attempts are counted per user, whatever the address). **Leave unset unless you know the exact address of your reverse proxy.** A wrong value lets a client spoof its rate-limit key by sending a forged `X-Forwarded-For` header — the panel trusts the last hop only when the TCP peer matches one of these CIDRs. With Apache on the host (the default), the panel sees the proxy's connections come from the Docker network's gateway, e.g. `172.18.0.1` (`docker network inspect` shows it; measured 2026-10-15: the panel's `127.0.0.1:8080` is published through Docker's proxy, and a connection to such a port from the host arrives from the gateway). | *(empty — XFF ignored)* | `.env` |
|
||||||
|
|
||||||
|
`PANEL_COOKIE_SECURE` is not in the table: the session cookie is `Secure`
|
||||||
|
unless it is `false`, which only a trial over plain HTTP wants, and the
|
||||||
|
compose file does not pass it - the local trial in the README sets it on
|
||||||
|
`docker run` (`-e PANEL_COOKIE_SECURE=false`).
|
||||||
|
|
||||||
TLS certificate paths (`TLS_CERT_FILE`, `TLS_KEY_FILE`) are fixed in
|
TLS certificate paths (`TLS_CERT_FILE`, `TLS_KEY_FILE`) are fixed in
|
||||||
[deploy/docker-compose.yml](../deploy/docker-compose.yml) to match the `./certs`
|
[deploy/docker-compose.yml](../deploy/docker-compose.yml) to match the `./certs`
|
||||||
@@ -295,6 +302,9 @@ browser that does not send `Sec-Fetch-Site` (Safari before 16.4, or any
|
|||||||
browser on a panel served over plain HTTP at a name): there the panel compares
|
browser on a panel served over plain HTTP at a name): there the panel compares
|
||||||
`Origin` with `Host` and refuses every change as cross-origin - the log says
|
`Origin` with `Host` and refuses every change as cross-origin - the log says
|
||||||
so explicitly, printing the `Sec-Fetch-Site`, `Origin` and `Host` it compared.
|
so explicitly, printing the `Sec-Fetch-Site`, `Origin` and `Host` it compared.
|
||||||
|
A request that carries neither header - a browser too old to send `Origin`,
|
||||||
|
or a script posting to the panel without one - is refused the same way
|
||||||
|
since 2.10.4.
|
||||||
|
|
||||||
In every case the proxy terminates HTTPS for the panel; the resulting
|
In every case the proxy terminates HTTPS for the panel; the resulting
|
||||||
certificate must end up under `./certs` as `fullchain.pem` and `privkey.pem` —
|
certificate must end up under `./certs` as `fullchain.pem` and `privkey.pem` —
|
||||||
@@ -418,7 +428,7 @@ changes.
|
|||||||
|---|---|---|
|
|---|---|---|
|
||||||
| Overview | Overview | `/overview` |
|
| Overview | Overview | `/overview` |
|
||||||
| Outbound | Domains · Log · Queue · DMARC reports | `/outbound/domains`, `/outbound/log`, `/outbound/queue`, `/outbound/dmarc` |
|
| Outbound | Domains · Log · Queue · DMARC reports | `/outbound/domains`, `/outbound/log`, `/outbound/queue`, `/outbound/dmarc` |
|
||||||
| Inbound | Domains | `/inbound/domains` |
|
| Inbound | Domains · Log · Filter lists (global role) | `/inbound/domains`, `/inbound/log`, `/inbound/filter-lists` |
|
||||||
| Server | Health · System log · Backup · Users · Settings | `/server/health`, `/server/log`, `/server/backup`, `/server/users`, `/server/settings` |
|
| Server | Health · System log · Backup · Users · Settings | `/server/health`, `/server/log`, `/server/backup`, `/server/users`, `/server/settings` |
|
||||||
| User menu (your name) | Account · Help · Sign out | `/account`, `/help`, `POST /logout` |
|
| User menu (your name) | Account · Help · Sign out | `/account`, `/help`, `POST /logout` |
|
||||||
|
|
||||||
@@ -429,6 +439,11 @@ menu: a group with nothing in it is absent. *Inbound* appears only with
|
|||||||
queue, answer 404 to a user with the *domain* role — the same answer a missing
|
queue, answer 404 to a user with the *domain* role — the same answer a missing
|
||||||
page gets.
|
page gets.
|
||||||
|
|
||||||
|
Two pages are outside the menu: `/license`, the AGPL-3.0 text the footer links
|
||||||
|
to, open to anyone; and `/server/components` (global role), the panel's
|
||||||
|
component kit - every building block of its pages on one page, a reference
|
||||||
|
for whoever works on the panel.
|
||||||
|
|
||||||
Pages that used to be one are now several. The old Status page is **Overview**
|
Pages that used to be one are now several. The old Status page is **Overview**
|
||||||
(one verdict per check) and **Server › Health** (the tables behind them). The
|
(one verdict per check) and **Server › Health** (the tables behind them). The
|
||||||
old Settings page is **Account** (your own name, e-mail and password) and
|
old Settings page is **Account** (your own name, e-mail and password) and
|
||||||
@@ -450,23 +465,23 @@ assumptions stay here in `docs/guide.md`.
|
|||||||
### Overview and Health
|
### Overview and Health
|
||||||
|
|
||||||
**Overview** (`/overview`, global role) shows the stamp — the worst of its
|
**Overview** (`/overview`, global role) shows the stamp — the worst of its
|
||||||
checks — and one card per check: machine (processor and memory), supervised
|
checks — with a sentence beside it ("Mail is flowing. One thing needs
|
||||||
processes (Postfix, OpenDKIM, panel), TLS certificate, queue, milter sockets
|
attention.", "Mail may not be flowing." when a check fails) and, under it,
|
||||||
and reverse DNS. A check the panel could not run shows as a warning, never as
|
each check that is not fine with its value; then one card per check: machine (processor and memory), supervised
|
||||||
fine. Below the cards are the outbound domain list and, with the inbound relay
|
processes (Postfix, the panel - whose roles include the DKIM signer - and
|
||||||
|
their helpers), TLS certificate, queue, milter sockets and reverse DNS. A
|
||||||
|
check the panel could not run shows as a warning, never as fine. Below the cards are the outbound domain list and, with the inbound relay
|
||||||
on, the inbound one. Each card leads to its table on **Server › Health**
|
on, the inbound one. Each card leads to its table on **Server › Health**
|
||||||
(`/server/health`) — the queue card to the Outbound queue — and Health has the
|
(`/server/health`) — the queue card to the Outbound queue — and Health has the
|
||||||
readings, in the order of what stops mail first:
|
readings, numbered in the order of what stops mail first:
|
||||||
|
|
||||||
- **Machine** — processor (core and thread counts), memory and swap of the
|
- **Processes** and **Milter sockets**: the DKIM signer's two - `dkim-signer · applications` for what
|
||||||
host (a memory limit set on the container is not reflected), and
|
applications send on 465/587, `dkim-signer · notifications` for the
|
||||||
per-interface network throughput and totals of the container's own network
|
panel's own mail - the sender check, the send log (the journal-milter),
|
||||||
interfaces, read from the kernel's counters; CPU and throughput are measured between refreshes, so they appear
|
with the inbound relay on the Inbound log's (the inbound-journal-milter,
|
||||||
one refresh after the page opens. A fully busy processor or a machine out of
|
fail-open like the send log's), and, with one configured, the spam filter. Postfix attaches the signer
|
||||||
memory is a warning, because both delay or kill the mail path; throughput is
|
strictly, so either of its rows down means the mail that goes through it
|
||||||
only reported.
|
waits.
|
||||||
- **Processes**, **TLS certificate** (validity and expiry) and **Milter
|
|
||||||
sockets** (OpenDKIM, the sender check and the journal-milter).
|
|
||||||
- **Sender check** — what the strict check of the `From:` line has cost
|
- **Sender check** — what the strict check of the `From:` line has cost
|
||||||
today, in the last 30 days and in the last 12 months: for how long the
|
today, in the last 30 days and in the last 12 months: for how long the
|
||||||
panel did not answer while Postfix was running, how many connections
|
panel did not answer while Postfix was running, how many connections
|
||||||
@@ -478,23 +493,52 @@ readings, in the order of what stops mail first:
|
|||||||
connects during an outage is counted too; a message refused in the middle
|
connects during an outage is counted too; a message refused in the middle
|
||||||
is one the panel stopped answering for after it had begun. A time marked ≈
|
is one the panel stopped answering for after it had begun. A time marked ≈
|
||||||
is at most what it shows: at that start supervisord could not say since
|
is at most what it shows: at that start supervisord could not say since
|
||||||
when Postfix ran. Days are UTC days. The card warns when
|
when Postfix ran. Days are those of the container's zone (`TZ`). The card warns when
|
||||||
a connection was turned away or a message refused in the last 24 hours. If these numbers do not stay
|
a connection was turned away or a message refused in the last 24 hours. If these numbers do not stay
|
||||||
at nothing, the panel is in the way of your mail more than it should be -
|
at nothing, the panel is in the way of your mail more than it should be -
|
||||||
report it.
|
report it.
|
||||||
|
- **TLS certificate** — validity and expiry, and whether its names cover
|
||||||
|
`SELFPOST_HOSTNAME` (the tag beside *Names*); a certificate for another
|
||||||
|
name fails, as a client that checks the name refuses it.
|
||||||
- **Hostname and reverse DNS** — compares `SELFPOST_HOSTNAME` against the PTR
|
- **Hostname and reverse DNS** — compares `SELFPOST_HOSTNAME` against the PTR
|
||||||
record the internet publishes for this server's IP (forward-confirmed
|
record the internet publishes for this server's IP (forward-confirmed
|
||||||
reverse DNS), see [Server-level DNS](#server-level-dns-ptrrdns); use
|
reverse DNS), see [Server-level DNS](#server-level-dns-ptrrdns); use
|
||||||
*Re-check DNS* after changing DNS.
|
*Re-check DNS* after changing DNS. *Greeting* is the `220` line Postfix
|
||||||
|
opens every connection with; it must name the hostname, and another name
|
||||||
|
there is a warning.
|
||||||
|
- **Notification mail** — whether the DNS records the panel's own mail is
|
||||||
|
signed and authorised with are in place (see [Notifications](#notifications)).
|
||||||
|
- **Machine** — processor (core and thread counts), memory and swap of the
|
||||||
|
host (a memory limit set on the container is not reflected), and
|
||||||
|
per-interface network throughput and totals of the container's own network
|
||||||
|
interfaces, read from the kernel's counters; CPU and throughput are measured between refreshes, so they appear
|
||||||
|
one refresh after the page opens. A fully busy processor or a machine out of
|
||||||
|
memory is a warning, because both delay or kill the mail path; throughput is
|
||||||
|
only reported.
|
||||||
|
- **Your address** (in the Configuration card) — the address the panel sees
|
||||||
|
your browser at, and whether it took it from `X-Forwarded-For`. Behind a
|
||||||
|
reverse proxy that the panel does not trust it is the proxy's own: every
|
||||||
|
visitor then shares one sign-in limit, and the card says what to set in
|
||||||
|
`TRUSTED_PROXY_CIDR`.
|
||||||
- **Configuration** — the **Reload configuration** button writes again, from
|
- **Configuration** — the **Reload configuration** button writes again, from
|
||||||
the database, every file the panel keeps for the mail path - OpenDKIM's
|
the database, every file the panel keeps for the mail path - the DKIM
|
||||||
tables, the Postfix sender map, the inbound relay maps and the filter lists
|
selector table (`/data/dkim/selectors`), the Postfix sender map, the
|
||||||
(with `INBOUND_RELAY_ENABLE=true`), the DMARC report maps (with
|
inbound relay maps and the filter lists (with `INBOUND_RELAY_ENABLE=true`),
|
||||||
`DMARC_REPORTS_ENABLE=true`) - and has OpenDKIM and Postfix re-read them:
|
the DMARC report maps (with `DMARC_REPORTS_ENABLE=true`) - removes the keys
|
||||||
|
of deleted domains, and has Postfix re-read them; the signer needs no
|
||||||
|
reload, it reads the table for every message. This is
|
||||||
what the first start after a restore does (see [Restore](#restore)). Use it
|
what the first start after a restore does (see [Restore](#restore)). Use it
|
||||||
if the daemons drifted from what the panel shows after manual edits under
|
if the daemons drifted from what the panel shows after manual edits under
|
||||||
`/data`. A step that fails is named on the page, and the panel's log says
|
`/data`. A step that fails is named on the page, and the panel's log says
|
||||||
why.
|
why.
|
||||||
|
- **Test message** — sends one message to an address you type, from
|
||||||
|
`postmaster@` the hostname, the way notifications go (through Postfix,
|
||||||
|
signed with the key of the server's own name). The page that opens is its
|
||||||
|
row in **Outbound › Log**, which follows it to the receiving server: sent,
|
||||||
|
put off or refused, with the lines of `mail.log`. It tests the server's
|
||||||
|
own path - TLS, its address, the records of its name - not a domain's,
|
||||||
|
which only an application's mail does; and whether it reached the inbox
|
||||||
|
only the mailbox can say. Ten an hour, global role.
|
||||||
|
|
||||||
### Queue and System log
|
### Queue and System log
|
||||||
|
|
||||||
@@ -513,18 +557,50 @@ readings, in the order of what stops mail first:
|
|||||||
six hours. It lives in the data volume, so it survives a container recreate
|
six hours. It lives in the data volume, so it survives a container recreate
|
||||||
along with the rest of the state — `./data/log/` on the host — but it is
|
along with the rest of the state — `./data/log/` on the host — but it is
|
||||||
*not* included in backups: it is diagnostics, not state.
|
*not* included in backups: it is diagnostics, not state.
|
||||||
|
- **The container log** (`docker compose logs selfpost`) — what the panel
|
||||||
|
itself says, its milters among it; none of it is in `mail.log`. A message
|
||||||
|
the DKIM signer cannot sign is not sent unsigned, it waits: Postfix
|
||||||
|
answers an application with a temporary error and the application sends
|
||||||
|
it again later. Why is a line of the container log that begins `dkim-signer:` and
|
||||||
|
ends `- deferring`, e.g. `dkim-signer: no signing key for example.com -
|
||||||
|
deferring` (the selector table has no line for the domain: *Reload
|
||||||
|
configuration* on Server › Health writes it again from the database) or
|
||||||
|
`dkim-signer: no sending domain for login "shop" - deferring`. The sender
|
||||||
|
check's lines begin `sender-check:`.
|
||||||
|
|
||||||
### Account
|
### Account
|
||||||
|
|
||||||
**Account** (`/account`, every role) is the signed-in user's own: **Profile**
|
**Account** (`/account`, every role) is the signed-in user's own: **Profile**
|
||||||
(username and e-mail) and **Password**. Saving the profile does not ask for the
|
(username and e-mail) and **Password**. Changing the username or the
|
||||||
password; changing the password asks for the current one and is rate-limited
|
notifications switch does not ask for the password; changing the e-mail does,
|
||||||
like a sign-in. The e-mail is the user's own address: it is where the panel
|
and so does changing the password, both on the limit of a sign-in's guesses
|
||||||
|
(ten in 15 minutes per user). The e-mail is the user's own address: it is where the panel
|
||||||
sends [notifications](#notifications) while *Send me notifications* is ticked
|
sends [notifications](#notifications) while *Send me notifications* is ticked
|
||||||
beside it, and what a domain's *Use my e-mail* button fills in (see
|
beside it, and what a domain's *Use my e-mail* button fills in (see
|
||||||
[Domain-level DNS](#domain-level-dns-spf-dkim-dmarc)). Application SASL logins
|
[Domain-level DNS](#domain-level-dns-spf-dkim-dmarc)). Application SASL logins
|
||||||
are separate and are not changed here.
|
are separate and are not changed here.
|
||||||
|
|
||||||
|
**A confirmed e-mail and a forgotten password.** A new address - saved under
|
||||||
|
Account, or typed for a user by the global role on the user form - is sent a
|
||||||
|
link that confirms it (`https://<SELFPOST_HOSTNAME>/confirm/…`), which works
|
||||||
|
once, for 24 hours, opened in a browser signed in as that account; its page
|
||||||
|
has a button, and only the button confirms. The mail does not name the
|
||||||
|
account, so a mistyped address cannot be confirmed by whoever reads it.
|
||||||
|
Account tags the address *confirmed* or *not confirmed* and, while it is not,
|
||||||
|
offers *Send the link again* (at most five links an hour to one address).
|
||||||
|
Notifications go to the address either way. Only a confirmed address can be
|
||||||
|
sent a reset link: *Forgot your password?* on the sign-in page asks for the
|
||||||
|
username and always answers the same - whether the name exists, and whether
|
||||||
|
its address is confirmed, is not said; another link is not sent within ten
|
||||||
|
minutes of the last - and the link
|
||||||
|
(`https://<SELFPOST_HOSTNAME>/login/reset/…`) works once, for 30 minutes, and
|
||||||
|
only while the account's address is still the one it was sent to. Setting the
|
||||||
|
new password signs every session of the account out. The links name the
|
||||||
|
server by `SELFPOST_HOSTNAME`, so the panel must be served at that name. An
|
||||||
|
account without a confirmed address recovers as before: the global role sets
|
||||||
|
a new password on the user form, and the only global user from the container
|
||||||
|
(see [Users](#users)).
|
||||||
|
|
||||||
### Settings
|
### Settings
|
||||||
|
|
||||||
**Server › Settings** (`/server/settings`, global role) holds what is true for
|
**Server › Settings** (`/server/settings`, global role) holds what is true for
|
||||||
@@ -534,7 +610,24 @@ prune cycle without a container restart), and, read-only, the level-1 rate
|
|||||||
limit that comes from `.env` ([Rate limiting — level 1](#rate-limiting--level-1-ip-backstop)).
|
limit that comes from `.env` ([Rate limiting — level 1](#rate-limiting--level-1-ip-backstop)).
|
||||||
There is no panel-wide DMARC report address: each domain has its own. The page
|
There is no panel-wide DMARC report address: each domain has its own. The page
|
||||||
also has the switch for [notifications](#notifications) and the two DNS records
|
also has the switch for [notifications](#notifications) and the two DNS records
|
||||||
that make them arrive.
|
that make them arrive, with when they were last checked; **Re-check DNS** at
|
||||||
|
the top of the page checks them again once they are published (results are
|
||||||
|
otherwise kept a few minutes).
|
||||||
|
|
||||||
|
The last box, **Time zone**, names the zone the panel is in: `TZ` in `.env`
|
||||||
|
([Environment variables](#environment-variables)), UTC when it is not set,
|
||||||
|
with its offset from UTC. Every time the panel shows is in that zone, and no
|
||||||
|
time on the screens says which - this box is where it is said. Every day it
|
||||||
|
counts is that zone's too: *today* on Health, the days of the statistics,
|
||||||
|
the days of the log filters. Postfix runs in the same zone, so the stamps of
|
||||||
|
the `mail.log` lines on Server › System log and on a message's page read the
|
||||||
|
same clock as the rest of the page. The database keeps its times in UTC, so
|
||||||
|
changing the zone is a restart and nothing more; the days counted before it
|
||||||
|
stay days of the old zone, a shift of a few hours in the statistics. A name
|
||||||
|
the image does not know leaves everything in UTC, and the box says so. One
|
||||||
|
thing keeps UTC on purpose: the day a DMARC report covers is its reporter's,
|
||||||
|
who counts days in UTC, so the lists and a report's title name that day; the
|
||||||
|
report's page gives its window's times in this zone and says which day it is.
|
||||||
|
|
||||||
### Notifications
|
### Notifications
|
||||||
|
|
||||||
@@ -554,7 +647,9 @@ assigned to them. A message goes to the e-mail on the user's
|
|||||||
[Account](#account) page and only while *Send me notifications* is ticked
|
[Account](#account) page and only while *Send me notifications* is ticked
|
||||||
there; a user without an e-mail gets none. **Server › Settings** has one switch
|
there; a user without an e-mail gets none. **Server › Settings** has one switch
|
||||||
that turns notifications off for everyone. With it off nothing is checked, so
|
that turns notifications off for everyone. With it off nothing is checked, so
|
||||||
what is wrong when it is turned on again is reported then.
|
what is wrong when it is turned on again is reported then. The first check runs
|
||||||
|
three minutes after the panel starts, then every hour; the panel sends at
|
||||||
|
most 60 notifications an hour, a ceiling nothing should come near.
|
||||||
|
|
||||||
Notifications are the panel's own mail. They are sent from
|
Notifications are the panel's own mail. They are sent from
|
||||||
`postmaster@<SELFPOST_HOSTNAME>`, one message per recipient, are not rows of
|
`postmaster@<SELFPOST_HOSTNAME>`, one message per recipient, are not rows of
|
||||||
@@ -610,7 +705,8 @@ What the *domain* role reaches inside what is assigned:
|
|||||||
| Overview, Server, the Outbound queue | yes | no (404) |
|
| Overview, Server, the Outbound queue | yes | no (404) |
|
||||||
| Outbound: domain page, applications, Domain settings, log | every domain | assigned domains |
|
| Outbound: domain page, applications, Domain settings, log | every domain | assigned domains |
|
||||||
| Outbound › DMARC reports | every report, with the ingest statistics | reports of assigned domains, without the ingest box |
|
| Outbound › DMARC reports | every report, with the ingest statistics | reports of assigned domains, without the ingest box |
|
||||||
| Inbound: domain page, upstream, recipients | every domain | assigned inbound domains |
|
| Inbound: domain page, upstream, recipients, log | every domain | assigned inbound domains |
|
||||||
|
| Inbound › Filter lists | yes | no (404) |
|
||||||
| Add or delete a domain, either direction | yes | no |
|
| Add or delete a domain, either direction | yes | no |
|
||||||
| Export a domain (Domain settings) | yes | assigned domains |
|
| Export a domain (Domain settings) | yes | assigned domains |
|
||||||
| Import a domain (Server › Backup) | yes | no |
|
| Import a domain (Server › Backup) | yes | no |
|
||||||
@@ -650,11 +746,62 @@ published, then `docker compose up -d`. The backup version check requires the ru
|
|||||||
to match the version that created a full backup — see [Fixed image
|
to match the version that created a full backup — see [Fixed image
|
||||||
tag](#fixed-image-tag).
|
tag](#fixed-image-tag).
|
||||||
|
|
||||||
|
**To 2.8.0** there is nothing to do. From 2.8.0 the panel signs outgoing
|
||||||
|
mail itself, in a milter of its own, and OpenDKIM is no longer in the image;
|
||||||
|
the keys, the selectors and the DNS records stay as they are. The keys move
|
||||||
|
from `data/opendkim/keys` to `data/dkim/keys`: on the first start the
|
||||||
|
entrypoint copies them, as the panel's user, into `data/dkim.new`, renamed
|
||||||
|
into place when the copy is whole, and says so in the container log
|
||||||
|
(`entrypoint: the DKIM keys were copied from /data/opendkim to /data/dkim;
|
||||||
|
/data/opendkim is left for a rollback`). A copy that fails stops the start -
|
||||||
|
the panel would otherwise give the server's own name a new key, and its
|
||||||
|
published record would no longer match. `data/dkim` is the panel's alone
|
||||||
|
(directories 0700, files 0600); so is `data/opendkim`, which is left as it
|
||||||
|
was, for a rollback, until the next release, and which a full backup leaves
|
||||||
|
out. The selector table, `data/dkim/selectors`, is written from the database
|
||||||
|
at every start, before the signer serves.
|
||||||
|
|
||||||
|
**To 2.9.0** from 2.8.x there is nothing to do: the first start removes
|
||||||
|
`data/opendkim`, the key tree 2.7.x read, which 2.8.x kept for a rollback,
|
||||||
|
and says so (`entrypoint: removed /data/opendkim, the key tree of 2.7.x;
|
||||||
|
the keys are under /data/dkim`); the keys under `data/dkim` stay as they
|
||||||
|
are. From 2.7.x, start 2.8.x once first: an image of 2.9.0 on a `data`
|
||||||
|
whose keys are under `data/opendkim` only stops at the start and says so,
|
||||||
|
rather than give the server's own name a new key. Rolling back from 2.9.x
|
||||||
|
to 2.8.x needs nothing; to 2.7.x, the old tree is gone - only a full backup
|
||||||
|
of 2.7.x brings it back.
|
||||||
|
|
||||||
|
**Rolling back to 2.7.x** from 2.8.x (only): the 2.7.x image signs with OpenDKIM
|
||||||
|
from `data/opendkim`, which holds the keys as they were at the upgrade. A
|
||||||
|
domain added under 2.8.x has no key there, and 2.7.x sends its mail
|
||||||
|
unsigned; one deleted and added again under 2.8.x has its old key there,
|
||||||
|
which no longer matches its published record. Postfix's queue under
|
||||||
|
`data/postfix/queue` belongs to Postfix's users, which have other numbers
|
||||||
|
in the two images (postfix 100 in 2.8.0, 101 in 2.7.x); a 2.7.x start
|
||||||
|
reads the queue before it hands it over and stops at `postfix check`
|
||||||
|
("scan_dir_push: open directory defer: Permission denied"). With the
|
||||||
|
instance stopped, hand the queue over in the 2.7.x image itself, by name,
|
||||||
|
before its first start (the image is the one you roll back to):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker run --rm -v ./data:/data --entrypoint sh ghcr.io/mixeme/selfpost:2.7.0 -c 'chown -R postfix:postfix /data/postfix/queue && chgrp postdrop /data/postfix/queue/public /data/postfix/queue/maildrop && chown root /data/postfix/queue /data/postfix/queue/pid'
|
||||||
|
```
|
||||||
|
|
||||||
|
(Measured on the test server, 2026-10-16, on a copy of its queue: 2.7.x's
|
||||||
|
`postsuper` failed before and passed after.) Going forward again needs
|
||||||
|
nothing for the queue - 2.8.0 hands it over itself. A domain added under
|
||||||
|
2.7.x has its key only in `data/opendkim/keys/<domain>/`, and the
|
||||||
|
entrypoint copies only while `data/dkim` holds no key at all: with the
|
||||||
|
instance stopped, copy that directory into `data/dkim/keys/` (the start
|
||||||
|
makes it the panel's). Removing `data/dkim` instead has the old tree copied
|
||||||
|
again whole and loses the keys of domains added under 2.8.x.
|
||||||
|
|
||||||
### Container health
|
### Container health
|
||||||
|
|
||||||
The image declares a Docker `HEALTHCHECK` that probes `GET /healthz` on port
|
The image declares a Docker `HEALTHCHECK` that probes `GET /healthz` on port
|
||||||
8080 (unauthenticated). It returns `200 ok` when OpenDKIM, the panel, and
|
8080 (unauthenticated). It returns `200 ok` when the panel - whose roles
|
||||||
Postfix are all `RUNNING` under supervisord; otherwise `503 unhealthy`. This
|
include the DKIM signer - and Postfix are both `RUNNING` under supervisord;
|
||||||
|
otherwise `503 unhealthy`. This
|
||||||
catches a dead mail path that would still leave the HTTP server up, but it
|
catches a dead mail path that would still leave the HTTP server up, but it
|
||||||
does **not** verify TLS certificates, DNS records, or end-to-end delivery —
|
does **not** verify TLS certificates, DNS records, or end-to-end delivery —
|
||||||
use the authenticated [Overview and Health](#overview-and-health) pages for
|
use the authenticated [Overview and Health](#overview-and-health) pages for
|
||||||
@@ -707,11 +854,17 @@ as themselves, the host's IPv6 address and `127.0.0.1` as `172.18.0.1`.
|
|||||||
|
|
||||||
### Full backup and restore
|
### Full backup and restore
|
||||||
|
|
||||||
|
Both downloads - the full backup on **Server › Backup** and a domain's
|
||||||
|
export on its settings - ask for your account password, the one you sign in
|
||||||
|
with, every time: a session left open is not enough to take them. Wrong
|
||||||
|
guesses count against the same limit as a password change (ten in fifteen
|
||||||
|
minutes).
|
||||||
|
|
||||||
**Full backup** is a self-contained project archive: `data/` (SQLite, all
|
**Full backup** is a self-contained project archive: `data/` (SQLite, all
|
||||||
domains' DKIM keys, all applications' SASL credentials, `manifest.json` with
|
domains' DKIM keys under `data/dkim/`, all applications' SASL credentials,
|
||||||
the version that created it), plus `docker-compose.yml`, `.env`, and `certs/`
|
`manifest.json` with the version that created it), plus `docker-compose.yml`,
|
||||||
from the operator directory next to `./data`. Delivery logs under `data/log/`
|
`.env`, and `certs/` from the operator directory next to `./data`. Delivery
|
||||||
are excluded, and so is the Postfix queue: the archive is taken while the
|
logs under `data/log/` are excluded, and so is the Postfix queue: the archive is taken while the
|
||||||
instance runs, and a copy of a queue Postfix is working on can hold a message
|
instance runs, and a copy of a queue Postfix is working on can hold a message
|
||||||
twice or not at all. Mail still waiting for delivery is therefore not carried
|
twice or not at all. Mail still waiting for delivery is therefore not carried
|
||||||
over, and a restored instance starts with an empty queue; to keep the queue,
|
over, and a restored instance starts with an empty queue; to keep the queue,
|
||||||
@@ -828,10 +981,10 @@ container of the **exact same image version** that created the backup — SelfPo
|
|||||||
refuses to start otherwise and tells you which tag to use. On the first
|
refuses to start otherwise and tells you which tag to use. On the first
|
||||||
successful start after restore, `data/manifest.json` from the archive is
|
successful start after restore, `data/manifest.json` from the archive is
|
||||||
**deleted** — it guards only that one boot, so a later in-place upgrade is not
|
**deleted** — it guards only that one boot, so a later in-place upgrade is not
|
||||||
blocked. On that same first boot the panel also runs one **Resync** — OpenDKIM's
|
blocked. On that same first boot the panel also runs one **Resync** — the DKIM
|
||||||
tables and Postfix's sender map are re-derived from SQLite (inbound relay maps
|
selector table and Postfix's sender map are re-derived from SQLite (inbound relay maps
|
||||||
when `INBOUND_RELAY_ENABLE=true`; DMARC maps when `DMARC_REPORTS_ENABLE=true`)
|
when `INBOUND_RELAY_ENABLE=true`; DMARC maps when `DMARC_REPORTS_ENABLE=true`)
|
||||||
and both daemons are reloaded, healing any drift between the extracted files and
|
and Postfix is reloaded, healing any drift between the extracted files and
|
||||||
the database. The manifest is deleted only after that Resync is done: a first
|
the database. The manifest is deleted only after that Resync is done: a first
|
||||||
start that fails on the way leaves it, and the next start is a restore again
|
start that fails on the way leaves it, and the next start is a restore again
|
||||||
and runs the Resync. The *Reload configuration* button on Server › Health runs the same Resync.
|
and runs the Resync. The *Reload configuration* button on Server › Health runs the same Resync.
|
||||||
@@ -874,7 +1027,7 @@ A version mismatch at step 5 refuses to start and leaves `/data` untouched —
|
|||||||
the panel exits with a message naming the tag to use, e.g.:
|
the panel exits with a message naming the tag to use, e.g.:
|
||||||
|
|
||||||
```
|
```
|
||||||
backup: this backup was created by SelfPost 2.6.1 but this image is 2.7.0 — restore into the matching image (selfpost:2.6.1)
|
backup: this backup was created by SelfPost 2.14.1 but this image is 2.14.2 — restore into the matching image (selfpost:2.14.1)
|
||||||
```
|
```
|
||||||
|
|
||||||
Fix the tag in `docker-compose.yml` (build that tag as under [Fixed image
|
Fix the tag in `docker-compose.yml` (build that tag as under [Fixed image
|
||||||
@@ -1003,7 +1156,10 @@ The CLI also *writes* encrypted backups for scripted/cron use. The password
|
|||||||
must be at least 12 characters, as on the panel's forms (decrypting takes
|
must be at least 12 characters, as on the panel's forms (decrypting takes
|
||||||
any), and it comes from `SELFPOST_BACKUP_PASSWORD` or `-password-file <path>`
|
any), and it comes from `SELFPOST_BACKUP_PASSWORD` or `-password-file <path>`
|
||||||
(first line), never from a command-line argument, which would be visible in
|
(first line), never from a command-line argument, which would be visible in
|
||||||
the process list.
|
the process list. Its other flags: `-o <file>` writes the output to a file
|
||||||
|
instead of standard output, `-i <file>` reads the encrypted archive from a
|
||||||
|
file instead of standard input (with `-decrypt`), and `-version` prints the
|
||||||
|
version.
|
||||||
`-e` with the name only takes the value from your shell's environment, so it is
|
`-e` with the name only takes the value from your shell's environment, so it is
|
||||||
not written on the command line:
|
not written on the command line:
|
||||||
|
|
||||||
@@ -1118,7 +1274,12 @@ anyone on the host; they hold addresses, not secrets.
|
|||||||
|
|
||||||
In the panel the lists are **Inbound › Filter lists** (global role), and what
|
In the panel the lists are **Inbound › Filter lists** (global role), and what
|
||||||
port 25 decided about each message is **Inbound › Log**: accepted mail with the
|
port 25 decided about each message is **Inbound › Log**: accepted mail with the
|
||||||
filter's verdict, refused mail with the reply it was given. A row of the log
|
filter's verdict, refused mail with the reply it was given. For accepted mail
|
||||||
|
its *Upstream* column says what the upstream made of it - *queued* (not tried
|
||||||
|
yet), *held* (in the queue by the filter's verdict, until released), *sent*,
|
||||||
|
*deferred* (a greylisting, an upstream that cannot be reached: Postfix tries
|
||||||
|
again) or *bounced* (refused for good, or expired in the queue; the sender
|
||||||
|
was sent a bounce) - and opens the last reply and the host that gave it. Rows logged before 2.10.0 have no upstream result. A row of the log
|
||||||
leads to the lists with its sender filled in. Beside each entry of a list is
|
leads to the lists with its sender filled in. Beside each entry of a list is
|
||||||
how often it decided a message, counted from the log - so over the days the
|
how often it decided a message, counted from the log - so over the days the
|
||||||
log keeps - and *never* marks an entry that may have outlived its use. The
|
log keeps - and *never* marks an entry that may have outlived its use. The
|
||||||
@@ -1142,7 +1303,8 @@ inbound mail is being refused with a temporary error (`tempfail`) or going throu
|
|||||||
(`accept`).
|
(`accept`).
|
||||||
|
|
||||||
**Inbound mail is not in the Outbound log.** Port 25 runs its own milter chain:
|
**Inbound mail is not in the Outbound log.** Port 25 runs its own milter chain:
|
||||||
the optional anti-spam milter, and nothing else. OpenDKIM does not sign relayed
|
the optional anti-spam milter and the panel's inbound journal (Inbound › Log,
|
||||||
|
fail-open), not the signer, the sender check or the outbound journal. The DKIM signer does not sign relayed
|
||||||
mail (and a signing outage therefore cannot hold it up), and the journal-milter
|
mail (and a signing outage therefore cannot hold it up), and the journal-milter
|
||||||
does not file it — the [Outbound log](#outbound-log) stays a record of what
|
does not file it — the [Outbound log](#outbound-log) stays a record of what
|
||||||
*you* sent.
|
*you* sent.
|
||||||
@@ -1199,7 +1361,11 @@ sees the *Ingest* box — whether ingest works, the last report received, report
|
|||||||
kept this week and parse failures. Open a domain's roll-up from the list or
|
kept this week and parse failures. Open a domain's roll-up from the list or
|
||||||
from *DMARC reports* in the domain page's menu. A user with the *domain* role
|
from *DMARC reports* in the domain page's menu. A user with the *domain* role
|
||||||
sees the reports of the domains assigned to them, without the *Ingest* box.
|
sees the reports of the domains assigned to them, without the *Ingest* box.
|
||||||
Summaries are pruned (500 kept for each domain, 90 days max).
|
Summaries are pruned (500 kept for each domain, 90 days max). A report's
|
||||||
|
page is `/outbound/dmarc/reports/<id>` and a domain's roll-up
|
||||||
|
`/outbound/dmarc/domains/<id>`. A report's day - in the lists and its title -
|
||||||
|
is the reporter's, counted in UTC as reports count their days; its page shows
|
||||||
|
the window's times in the panel's zone, with a line naming that day.
|
||||||
|
|
||||||
A message that is not a report the relay keeps — unreadable as an aggregate
|
A message that is not a report the relay keeps — unreadable as an aggregate
|
||||||
report, about a domain not sent from here, or delivered to another domain's
|
report, about a domain not sent from here, or delivered to another domain's
|
||||||
@@ -1259,7 +1425,7 @@ listed for it. Since `2.4.0` that rule covers both senders a message has:
|
|||||||
|
|
||||||
- the **envelope sender** (`MAIL FROM`), which Postfix checks;
|
- the **envelope sender** (`MAIL FROM`), which Postfix checks;
|
||||||
- the **`From:` line** of the message - the one a mail program shows and the
|
- the **`From:` line** of the message - the one a mail program shows and the
|
||||||
one the DKIM signature is chosen by - which the panel checks.
|
one DMARC holds the DKIM signature's domain to - which the panel checks.
|
||||||
|
|
||||||
An address with an extension counts as the address: an application listed as
|
An address with an extension counts as the address: an application listed as
|
||||||
`invoices@…` may also send as `invoices+march@…`, in the envelope and in
|
`invoices@…` may also send as `invoices+march@…`, in the envelope and in
|
||||||
@@ -1272,17 +1438,30 @@ Postfix asks who owns the address before it asks who owns the domain.
|
|||||||
A message whose `From:` is anything else is refused with `550 5.7.1` and a
|
A message whose `From:` is anything else is refused with `550 5.7.1` and a
|
||||||
text naming the line: another of your domains, the server's own name, an
|
text naming the line: another of your domains, the server's own name, an
|
||||||
address at a domain that is not on this server, a line with two addresses,
|
address at a domain that is not on this server, a line with two addresses,
|
||||||
or no `From:` at all. The line is read the way the signer, OpenDKIM, reads
|
or no `From:` at all. The line must be one address as the mail standard
|
||||||
it - the panel runs OpenDKIM's own parser - so only its address counts: what a
|
(RFC 5322) writes it - the address a mail program shows - and only that
|
||||||
display name or a comment around it says is not judged, and a line with more
|
address counts: what a display name or a comment around it says is not
|
||||||
than one address is refused. A display name with a comma or brackets has to
|
judged, and a line with more than one address is refused. A display name
|
||||||
be quoted (`"Shop, Inc." <orders@…>`), as the mail standard asks and mail
|
with a comma or brackets has to be quoted (`"Shop, Inc." <orders@…>`), as
|
||||||
libraries do; unquoted, OpenDKIM may take a word of the name for the
|
the standard asks and mail libraries do; unquoted, the line does not read
|
||||||
address. The refusal
|
as one address and is refused. The address is held to the application's
|
||||||
|
list the way Postfix holds the envelope sender, a quoted local part as the
|
||||||
|
same address unquoted. The refusal
|
||||||
is permanent, so the application does not send the message again; it is a
|
is permanent, so the application does not send the message again; it is a
|
||||||
row of the [Outbound log](#outbound-log) tagged `wrong From`, and the row's
|
row of the [Outbound log](#outbound-log) tagged `wrong From`, and the row's
|
||||||
page shows what the line said.
|
page shows what the line said.
|
||||||
|
|
||||||
|
Since `2.8.0` a **`Sender:` line** - the one a mail program shows beside
|
||||||
|
`From:` as "on behalf of" - is held to the same rule when the message has
|
||||||
|
one: one address, one the application may send as, and no more than one
|
||||||
|
such line. Otherwise the message is refused the same way, `550 5.7.1
|
||||||
|
Sender: …`, tagged `wrong Sender`. Most mail libraries write no `Sender:`;
|
||||||
|
one that writes the application's own address passes. A system that writes
|
||||||
|
for people outside your domains - a ticket system, a CRM - puts its own
|
||||||
|
address in `From:` with the person's name (`"Jane via Support"
|
||||||
|
<support@…>`) and the person's address in `Reply-To:`: their domain in
|
||||||
|
`From:` would fail DMARC at the receiver anyway.
|
||||||
|
|
||||||
What this stops working when you upgrade from an earlier version:
|
What this stops working when you upgrade from an earlier version:
|
||||||
|
|
||||||
- an application that sends with the `From:` of another of your domains -
|
- an application that sends with the `From:` of another of your domains -
|
||||||
@@ -1291,7 +1470,10 @@ What this stops working when you upgrade from an earlier version:
|
|||||||
differ - add the second address to the list;
|
differ - add the second address to the list;
|
||||||
- a contact form that puts its visitor's address into `From:` - put it into
|
- a contact form that puts its visitor's address into `From:` - put it into
|
||||||
`Reply-To:` and send from an address of your own, which is also what
|
`Reply-To:` and send from an address of your own, which is also what
|
||||||
DMARC requires.
|
DMARC requires;
|
||||||
|
- (`2.8.0`) a message with a `Sender:` of another domain, or of an address
|
||||||
|
the application is not listed for - drop the line or write the
|
||||||
|
application's own address there.
|
||||||
|
|
||||||
The check is strict: while the panel does not answer, Postfix answers with a
|
The check is strict: while the panel does not answer, Postfix answers with a
|
||||||
temporary error (`451`, and `454` to the login): an application that queues
|
temporary error (`451`, and `454` to the login): an application that queues
|
||||||
@@ -1299,6 +1481,23 @@ its mail sends it again later, one that sends directly gets an error to retry;
|
|||||||
nothing is accepted unchecked. **Server › Health** counts how often
|
nothing is accepted unchecked. **Server › Health** counts how often
|
||||||
that happens.
|
that happens.
|
||||||
|
|
||||||
|
Since `2.8.0` the check also guards the signature: the panel signs an
|
||||||
|
application's message with the key of the domain its login belongs to, and
|
||||||
|
DMARC counts that signature only when its domain aligns with the domain of
|
||||||
|
`From:`. Without the check an application could send under this server's
|
||||||
|
signature as another of its domains, or as the server itself.
|
||||||
|
|
||||||
|
**Lines of more than 998 bytes.** The signer signs the message as the
|
||||||
|
application handed it over. Postfix's SMTP client breaks a body line longer
|
||||||
|
than 998 bytes when it delivers the message (`smtp_line_length_limit`),
|
||||||
|
after the signature was made, and the receiver's check of the signature
|
||||||
|
then fails. Such a message is still sent; the container log names its
|
||||||
|
domain and login (`dkim-signer: example.com (login "shop"): a body line
|
||||||
|
longer than 998 bytes - ...`). Mail libraries keep lines short with
|
||||||
|
quoted-printable or base64; an application that writes its own body has to
|
||||||
|
as well. A message whose header section is over 1 MiB is refused,
|
||||||
|
`552 5.3.4`.
|
||||||
|
|
||||||
*Export domain* writes a single-domain file; *Import a domain* on *Server ›
|
*Export domain* writes a single-domain file; *Import a domain* on *Server ›
|
||||||
Backup* reads one back in (**global role only**) — see [Exporting and importing
|
Backup* reads one back in (**global role only**) — see [Exporting and importing
|
||||||
a single domain](#exporting-and-importing-a-single-domain).
|
a single domain](#exporting-and-importing-a-single-domain).
|
||||||
@@ -1374,9 +1573,10 @@ gateway, not its own address (see [Rate limiting — level
|
|||||||
1](#rate-limiting--level-1-ip-backstop)).
|
1](#rate-limiting--level-1-ip-backstop)).
|
||||||
|
|
||||||
It is **not an authentication boundary.** The SASL login itself still succeeds;
|
It is **not an authentication boundary.** The SASL login itself still succeeds;
|
||||||
the check runs in the journal-milter, on the same fail-open path as level 2
|
the check runs in the journal-milter and is fail-open like level 2 (below):
|
||||||
(below), so if that milter does not answer the restriction is skipped rather
|
if that milter does not answer, or its lookup hits a store error, the
|
||||||
than enforced. Use it to narrow where an application may send from, not to contain a
|
restriction is skipped rather than enforced. A message whose client address
|
||||||
|
Postfix did not pass is refused, not let through. Use it to narrow where an application may send from, not to contain a
|
||||||
leaked password — for that, regenerate the password.
|
leaked password — for that, regenerate the password.
|
||||||
|
|
||||||
**Level 2 — application** — optional override of the domain limit for one
|
**Level 2 — application** — optional override of the domain limit for one
|
||||||
@@ -1397,8 +1597,9 @@ hours and on demand via **Recalculate now**.
|
|||||||
|
|
||||||
**Level 2 is best-effort, not a guarantee.** It runs inside the
|
**Level 2 is best-effort, not a guarantee.** It runs inside the
|
||||||
journal-milter and is deliberately fail-open: if the rate-limit lookup hits
|
journal-milter and is deliberately fail-open: if the rate-limit lookup hits
|
||||||
a store error, or the connecting client's IP is not available to the
|
a store error, level 2 is skipped and the message is accepted rather than
|
||||||
milter, level 2 is skipped and the message is accepted rather than held up.
|
held up. The client address plays no part: a ceiling is on a domain or an
|
||||||
|
application, wherever its mail comes from.
|
||||||
The same applies if the journal-milter itself does not answer — Postfix is
|
The same applies if the journal-milter itself does not answer — Postfix is
|
||||||
configured to accept rather than refuse it when it cannot reach it. (Since
|
configured to accept rather than refuse it when it cannot reach it. (Since
|
||||||
`2.4.0` a panel that is down altogether makes Postfix refuse mail with a
|
`2.4.0` a panel that is down altogether makes Postfix refuse mail with a
|
||||||
@@ -1409,9 +1610,9 @@ the only one of the two that does not depend on the panel process.
|
|||||||
|
|
||||||
### Outbound log
|
### Outbound log
|
||||||
|
|
||||||
**Outbound › Log** (`/outbound/log`) is a searchable send log with server-side
|
**Outbound › Log** (`/outbound/log`) is the send log, with server-side
|
||||||
filters by domain, application, status and day (from and to, UTC days, as on
|
filters by domain, application, status and day (from and to, days of the container's zone, as on
|
||||||
the Inbound log); the status *rejected* is the three kinds of refusal
|
the Inbound log); the status *rejected* is the four kinds of refusal
|
||||||
together, which the table tells apart by their tags. A user with the *domain*
|
together, which the table tells apart by their tags. A user with the *domain*
|
||||||
role sees only the rows of their domains. The filters are kept by the pages,
|
role sees only the rows of their domains. The filters are kept by the pages,
|
||||||
the refresh and the way back from a message, and the log refreshes its rows
|
the refresh and the way back from a message, and the log refreshes its rows
|
||||||
@@ -1428,19 +1629,20 @@ the row as a failure rather than show a delivery it cannot evidence, and
|
|||||||
counts that it did. A
|
counts that it did. A
|
||||||
refused row says why in its tag: `rate-limited` (a [level-2 rate
|
refused row says why in its tag: `rate-limited` (a [level-2 rate
|
||||||
limit](#rate-limiting--level-2-domain-and-application)), `wrong client` (the
|
limit](#rate-limiting--level-2-domain-and-application)), `wrong client` (the
|
||||||
application's client IP list) or `wrong From` (see [Who an application may
|
application's client IP list), `wrong From` or `wrong Sender` (see [Who an
|
||||||
send as](#who-an-application-may-send-as)); a row refused before `2.4.0`
|
application may send as](#who-an-application-may-send-as)); a row refused before `2.4.0`
|
||||||
says `rejected`. A refused message's page shows what Postfix wrote about the
|
says `rejected`. A refused message's page shows what Postfix wrote about the
|
||||||
refusal: for `wrong From` the lines of the queue id the message had, for the
|
refusal: for `wrong From` and `wrong Sender` the lines of the queue id the
|
||||||
other two the one `NOQUEUE` line; a row refused before `2.5.1` says the line
|
message had, for the other two the one `NOQUEUE` line; a row refused before `2.5.1` says the line
|
||||||
was not kept, and one the panel wrote from `mail.log` after a restart - it
|
was not kept, and one the panel wrote from `mail.log` after a restart - it
|
||||||
had stopped before writing the refusal itself - says so;
|
had stopped before writing the refusal itself - says so;
|
||||||
*Details* opens that row's own page (`/outbound/log/{id}`). That page carries
|
*Details* opens that row's own page (`/outbound/log/{id}`). That page carries
|
||||||
the sending domain, the application it was submitted under, the Postfix
|
the sending domain, the application it was submitted under, the Postfix
|
||||||
queue id and the journal id, beside the message's history — when it was
|
queue id and the journal id, beside the message's history — when it was
|
||||||
accepted and what Postfix later reported for the recipient. A `deferred`
|
accepted and what Postfix later reported for the recipient. A `deferred`
|
||||||
or `bounced` row includes this Postfix's retry intervals (first delay,
|
row includes this Postfix's retry intervals (first delay, backoff cap, queue
|
||||||
backoff cap, queue lifetime), the same numbers the Outbound queue shows; users
|
lifetime), a `bounced` one the queue lifetime, the same numbers the Outbound
|
||||||
|
queue shows; users
|
||||||
with the *domain* role see them here because they cannot open the queue. Under
|
with the *domain* role see them here because they cannot open the queue. Under
|
||||||
both sit the `mail.log` lines for its queue id: the connection to the
|
both sit the `mail.log` lines for its queue id: the connection to the
|
||||||
receiving server, the server's reply, and the status that reply was filed
|
receiving server, the server's reply, and the status that reply was filed
|
||||||
@@ -1450,7 +1652,7 @@ the row is deleted by retention, and not while another row carries the same
|
|||||||
queue id (Postfix reuses ids); a
|
queue id (Postfix reuses ids); a
|
||||||
message accepted before `2.2.0` shows them only while they are still in the
|
message accepted before `2.2.0` shows them only while they are still in the
|
||||||
current file, and the page says so when there are none. Retention is set under **Server › Settings** (global
|
current file, and the page says so when there are none. Retention is set under **Server › Settings** (global
|
||||||
role); `SEND_LOG_RETENTION_DAYS` in `.env` is only the initial default until it
|
role; with the inbound relay on, the Inbound log's has a field of its own there); `SEND_LOG_RETENTION_DAYS` in `.env` is only the initial default until it
|
||||||
is changed there.
|
is changed there.
|
||||||
|
|
||||||
### Exporting and importing a single domain
|
### Exporting and importing a single domain
|
||||||
|
|||||||
@@ -68,6 +68,7 @@ to the days the row's own message could still be in the queue; this is the
|
|||||||
bound the roadmap item asked for. The search of the current file that the
|
bound the roadmap item asked for. The search of the current file that the
|
||||||
page did before had the same exposure for a day. `enable_long_queue_ids`
|
page did before had the same exposure for a day. `enable_long_queue_ids`
|
||||||
would end it and is a change to the mail path, not part of this item.
|
would end it and is a change to the mail path, not part of this item.
|
||||||
|
*It was made in `2.10.1` (owner, 2026-10-16).*
|
||||||
|
|
||||||
### Retention
|
### Retention
|
||||||
|
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user