release: 1.0.0: Public 1.0 image; delivery log; docs and SECURITY.md (incl. 1.0.1)
[1.0.0]
Added:
- A **Delivery log** on each delivery's page (`/deliveries/{id}`): the
- A **History** block on the same page: the journal's two timestamps stated as
Fixed:
- Postfix config: copy TLS cert/key from the (often `:ro`, host-owned) mount
- Postfix config: allow `maillog_file` under `/data` via
- E2e: read `/data/setup-token` via `docker compose exec` (file is `0600`
- Entrypoint: check `SELFPOST_HOSTNAME` before `/data` setup (so a bad identity
- E2e gate: wait for host-published `/healthz` before panel setup, and stop
… (3 more in CHANGELOG)
Changed:
- Document what `.spbk` and `.spde` stand for (SelfPost backup / SelfPost domain
- Docs aligned with the code: setup URL is `/setup/<token>` (README and
- Full backups no longer carry `/data/log`. It is Postfix's raw log plus its
- Monitoring screens (status, mail queue, system log, deliveries) use adaptive
- Documentation package consolidated into `docs/development.md`: Documentation
… (4 more in CHANGELOG)
[1.0.1]
A documentation and packaging release: no change to the mail path, the database, or the on-disk layout. Upgrading is a tag bump.
Added:
- `SECURITY.md` — how to report a vulnerability privately (GitHub private
- [docs/plans/](docs/plans/) — one document per agreed extension: the optional
Changed:
- The panel's **Account** entry is now called **Settings** — nav link, page
- The signed-in name in the panel's nav is now labelled `User:`, so it reads as
- [docs/product.md](docs/product.md) reframes the future line: the inbound
- [docs/security.md](docs/security.md) is now in English, matching the rest of
- The README documentation table now points at `SECURITY.md` for reporting a
… (2 more in CHANGELOG)
Fixed:
- The panel's static assets are served with a content ETag and
Squashed CHANGELOG sections: [1.0.0], [1.0.1].
Full detail: CHANGELOG.md (1.0.0, 1.0.1).
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
{{define "content"}}
|
||||
<h1>Account</h1>
|
||||
<h1>Settings</h1>
|
||||
|
||||
{{if .Flash}}<div class="flash">{{.Flash}}</div>{{end}}
|
||||
|
||||
|
||||
@@ -11,7 +11,8 @@
|
||||
<p class="muted"><strong>The backup file is a secret</strong> (it contains
|
||||
private keys and credentials). Store and transfer it securely and delete it
|
||||
once the restore succeeds. Encrypting it below is the simplest way to do that:
|
||||
the download is then a <code>.spbk</code> file that only the password opens.</p>
|
||||
the download is then a <code>.spbk</code> file (SelfPost backup) that only
|
||||
the password opens.</p>
|
||||
{{if .BackupErr}}<p class="error">{{.BackupErr}}</p>{{end}}
|
||||
<form method="post" action="/backup">
|
||||
{{template "encryptfields" .}}
|
||||
@@ -22,9 +23,10 @@
|
||||
<div class="card">
|
||||
<h2>Import a domain</h2>
|
||||
<p class="muted">Move a single domain here from another SelfPost instance using
|
||||
a domain export file (from that domain's page). Its DKIM key and application
|
||||
passwords come across, so the published DNS record needs no change. The export
|
||||
file is a secret, like a full backup.</p>
|
||||
a domain export file (from that domain's page) — plain <code>.json</code> or
|
||||
encrypted <code>.spde</code> (SelfPost domain export). Its DKIM key and
|
||||
application passwords come across, so the published DNS record needs no
|
||||
change. The export file is a secret, like a full backup.</p>
|
||||
{{if .ImportErr}}<p class="error">{{.ImportErr}}</p>{{end}}
|
||||
<form method="post" action="/domains/import" enctype="multipart/form-data">
|
||||
<label for="importfile">Domain export file</label>
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
{{/* The send log is six columns of addresses, subjects and timestamps, so this
|
||||
page takes the whole column rather than the reading measure (see the "wide"
|
||||
block in layout.html). A single delivery's page carries prose and keeps the
|
||||
measure, which is why this is declared per page and not per page name. */}}
|
||||
block in layout.html). It is declared per page rather than per page name
|
||||
because how wide a page needs to be is a property of that page, not of the
|
||||
navigation entry several pages share. */}}
|
||||
{{define "wide"}}wide{{end}}
|
||||
|
||||
{{define "content"}}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{{define "deliveries_rows"}}
|
||||
<div id="deliveries-rows" hx-get="/deliveries/rows?domain={{.FilterDomain}}&app={{.FilterApp}}&p={{.Page}}"
|
||||
hx-trigger="every 5s" hx-swap="outerHTML">
|
||||
<div id="deliveries-rows" data-poll hx-get="/deliveries/rows?domain={{.FilterDomain}}&app={{.FilterApp}}&p={{.Page}}"
|
||||
hx-trigger="load" hx-swap="outerHTML">
|
||||
{{if .Rows}}
|
||||
<table>
|
||||
<thead>
|
||||
|
||||
@@ -1,55 +1,124 @@
|
||||
{{/* One message's page. It takes the whole column rather than the reading
|
||||
measure (see the "wide" block in layout.html): the two cards below stand
|
||||
side by side, and under them are raw mail.log lines, which are long by
|
||||
nature and were the reason the send log opted out too. */}}
|
||||
{{define "wide"}}wide{{end}}
|
||||
|
||||
{{define "content"}}
|
||||
<h1>Delivery</h1>
|
||||
{{/* The subject is the page's name — it is what the message was about, and
|
||||
what an operator arriving from the log is holding in their head. The route
|
||||
under it is the other half of the identity: who it was from and who it was
|
||||
for, with the outcome badged on the end so the answer is on the first line
|
||||
of the page rather than somewhere in the cards below. */}}
|
||||
<h1 class="subject">{{if .Row.Subject}}{{.Row.Subject}}{{else}}(no subject){{end}}</h1>
|
||||
<p class="route">
|
||||
<span class="addr">{{.Row.From}}</span>
|
||||
<span class="arrow" aria-hidden="true">→</span>
|
||||
<span class="addr">{{if .Row.To}}{{.Row.To}}{{else}}—{{end}}</span>
|
||||
<span class="st st-{{.Level}}">{{.Row.Status}}</span>
|
||||
</p>
|
||||
|
||||
<a class="back" href="{{.BackURL}}">← Back to deliveries</a>
|
||||
|
||||
<div class="card">
|
||||
<h2>Message</h2>
|
||||
<p class="muted">One recipient of one message, as the journal-milter recorded
|
||||
it when Postfix accepted it. The delivery log lists these; everything the list
|
||||
has no room for is here.</p>
|
||||
{{/* The two columns: what was recorded on the left, in what order it happened
|
||||
on the right. They are a pair — the facts are only worth reading against
|
||||
the history and the history only means anything for a known message — so
|
||||
they are read together rather than one after the other (.split in
|
||||
panel.css lays them down into one column when there is no room). */}}
|
||||
<div class="split">
|
||||
<div class="card" id="message">
|
||||
<h2>Message</h2>
|
||||
<p class="muted">What the journal recorded as Postfix accepted this
|
||||
message. The delivery log lists only what identifies a row at a glance;
|
||||
everything the table has no column for is here.</p>
|
||||
|
||||
<label>Time</label>
|
||||
<span class="code">{{.Row.CreatedAt.Format "2006-01-02 15:04:05"}} UTC</span>
|
||||
<div class="facts">
|
||||
<div class="fact">
|
||||
<span class="fact-label">Domain</span>
|
||||
<span class="fact-value">{{if .Row.Domain}}{{.Row.Domain}}{{else}}—{{end}}</span>
|
||||
</div>
|
||||
<div class="fact">
|
||||
<span class="fact-label">Application</span>
|
||||
<span class="fact-value">{{if .Row.AppLogin}}{{.Row.AppLogin}}{{else}}—{{end}}</span>
|
||||
</div>
|
||||
<div class="fact">
|
||||
<span class="fact-label">Accepted</span>
|
||||
<span class="fact-value">{{.Row.CreatedAt.Format "2006-01-02 15:04:05"}} UTC</span>
|
||||
</div>
|
||||
<div class="fact">
|
||||
<span class="fact-label">Status reported</span>
|
||||
<span class="fact-value">{{.Row.UpdatedAt.Format "2006-01-02 15:04:05"}} UTC</span>
|
||||
</div>
|
||||
{{/* The queue id is monospace because it is an identifier to compare
|
||||
character by character against a log line, not a word to read. */}}
|
||||
<div class="fact">
|
||||
<span class="fact-label">Queue id</span>
|
||||
<span class="fact-value mono">{{if .Row.QueueID}}{{.Row.QueueID}}{{else}}—{{end}}</span>
|
||||
</div>
|
||||
<div class="fact">
|
||||
<span class="fact-label">Journal id</span>
|
||||
<span class="fact-value mono">{{.Row.ID}}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<label>From</label>
|
||||
<span class="code">{{.Row.From}}</span>
|
||||
{{if not .Row.QueueID}}
|
||||
<p class="muted">A message with no queue id was refused before Postfix
|
||||
queued it — under a level-2 rate limit — so it has no delivery attempt to
|
||||
show.</p>
|
||||
{{end}}
|
||||
</div>
|
||||
|
||||
<label>To</label>
|
||||
<span class="code">{{if .Row.To}}{{.Row.To}}{{else}}—{{end}}</span>
|
||||
<div class="card" id="history">
|
||||
<h2>History</h2>
|
||||
<p class="muted">A row is written when the message is accepted and updated
|
||||
once when Postfix reports the attempt for this recipient, so these are the
|
||||
two moments the journal knows about — not a trace of the SMTP conversation,
|
||||
which is in the delivery log below.</p>
|
||||
|
||||
<label>Subject</label>
|
||||
<span class="code">{{if .Row.Subject}}{{.Row.Subject}}{{else}}—{{end}}</span>
|
||||
|
||||
<label>Status</label>
|
||||
<span class="code">{{.Row.Status}}</span>
|
||||
<ol class="timeline">
|
||||
{{range .Events}}
|
||||
<li class="event lvl-{{.Level}}{{if .At.IsZero}} pending{{end}}">
|
||||
<p class="event-time">{{if .At.IsZero}}not yet{{else}}{{.At.Format "2006-01-02 15:04:05"}} UTC{{end}}</p>
|
||||
<p class="event-title"><span class="st st-{{.Level}}">{{.Status}}</span> {{.Title}}</p>
|
||||
<p class="event-detail muted">{{.Detail}}</p>
|
||||
</li>
|
||||
{{end}}
|
||||
</ol>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<h2>Origin</h2>
|
||||
<p class="muted">Which sending domain and which application's SASL account the
|
||||
message was submitted under — the two filters the delivery log offers.</p>
|
||||
{{/* Under both, at the column's full width: what Postfix itself wrote. The
|
||||
queue id used to be printed on this page as something to go and search the
|
||||
system log for by hand — this is that search, already done. */}}
|
||||
<div class="card" id="delivery-log">
|
||||
<h2>Delivery log</h2>
|
||||
<p class="muted">The lines Postfix wrote about this message in
|
||||
<code>mail.log</code>, oldest first — the connection to the receiving server,
|
||||
its reply, and the status that reply was filed as. Only lines carrying this
|
||||
message's queue id are shown; the whole log is on the
|
||||
<a href="/system-log">System log</a> page.</p>
|
||||
|
||||
<label>Domain</label>
|
||||
<span class="code">{{if .Row.Domain}}{{.Row.Domain}}{{else}}—{{end}}</span>
|
||||
|
||||
<label>Application</label>
|
||||
<span class="code">{{if .Row.AppLogin}}{{.Row.AppLogin}}{{else}}—{{end}}</span>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<h2>Handling</h2>
|
||||
<p class="muted">Postfix's own identifiers for this message. The queue id is
|
||||
what to search the system log for; a rejected message never got one, because
|
||||
it was refused before it was queued.</p>
|
||||
|
||||
<label>Queue id</label>
|
||||
<span class="code">{{if .Row.QueueID}}{{.Row.QueueID}}{{else}}—{{end}}</span>
|
||||
|
||||
<label>Status last reported</label>
|
||||
<span class="code">{{.Row.UpdatedAt.Format "2006-01-02 15:04:05"}} UTC</span>
|
||||
|
||||
<label>Journal id</label>
|
||||
<span class="code">{{.Row.ID}}</span>
|
||||
{{if .LogRows}}
|
||||
{{/* Two columns, the way the send log itself is a table: the times down one
|
||||
edge are what the message's pace is read off — the seconds between the
|
||||
connection and the reply — and lining them up is what makes that
|
||||
readable. A line whose head was not a timestamp keeps its whole text in
|
||||
the second column and leaves the first empty. */}}
|
||||
<table class="log">
|
||||
<thead>
|
||||
<tr><th>Time</th><th>Message</th></tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{{range .LogRows}}
|
||||
<tr>
|
||||
<td class="time muted">{{if .Time}}{{.Time}}{{else}}—{{end}}</td>
|
||||
<td class="log-text">{{.Text}}</td>
|
||||
</tr>
|
||||
{{end}}
|
||||
</tbody>
|
||||
</table>
|
||||
{{else}}
|
||||
<p class="muted">{{.LogNote}}</p>
|
||||
{{end}}
|
||||
</div>
|
||||
{{end}}
|
||||
|
||||
@@ -299,7 +299,7 @@
|
||||
<p class="muted"><strong>The export file is a secret</strong> — it contains the
|
||||
private DKIM key and application passwords. Transfer it securely and delete it
|
||||
after the import, or encrypt it below and move a <code>.spde</code> file
|
||||
instead; the import form asks for the password.</p>
|
||||
(SelfPost domain export) instead; the import form asks for the password.</p>
|
||||
{{if .ExportErr}}<p class="error">{{.ExportErr}}</p>{{end}}
|
||||
<form method="post" action="/domains/{{.Domain.ID}}/export">
|
||||
{{template "encryptfields" .}}
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
{{/* Password fields shared by the full-backup and domain-export forms. The
|
||||
checkbox is the switch: unticked, the download keeps its historic plain
|
||||
form (.tar.gz / .json); ticked, the file is sealed in a password-encrypted
|
||||
envelope (.spbk / .spde). The fields start hidden and are revealed by
|
||||
panel.js — with JavaScript blocked they are simply always visible, and the
|
||||
server still decides from the checkbox alone. */}}
|
||||
envelope (.spbk = SelfPost backup, .spde = SelfPost domain export). The
|
||||
fields start hidden and are revealed by panel.js — with JavaScript blocked
|
||||
they are simply always visible, and the server still decides from the
|
||||
checkbox alone. */}}
|
||||
{{define "encryptfields"}}
|
||||
<div class="encrypt">
|
||||
<label class="check">
|
||||
|
||||
@@ -69,8 +69,8 @@
|
||||
</div>
|
||||
{{template "sections" .}}
|
||||
<div class="session">
|
||||
<span class="muted">{{.User}}</span>
|
||||
{{if eq .Active "account"}}<span aria-current="page">{{template "icon-account"}}Account</span>{{else}}<a href="/account">{{template "icon-account"}}Account</a>{{end}}
|
||||
<span class="muted">User: {{.User}}</span>
|
||||
{{if eq .Active "account"}}<span aria-current="page">{{template "icon-account"}}Settings</span>{{else}}<a href="/account">{{template "icon-account"}}Settings</a>{{end}}
|
||||
<form class="inline" method="post" action="/logout">
|
||||
<button type="submit" class="danger">{{template "icon-sign-out"}}Sign out</button>
|
||||
</form>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
{{define "mail_queue_body"}}
|
||||
<div id="mail-queue-body" hx-get="/mail-queue/body" hx-trigger="every 5s" hx-swap="outerHTML">
|
||||
<div id="mail-queue-body" data-poll hx-get="/mail-queue/body" hx-trigger="load" hx-swap="outerHTML">
|
||||
{{if .Error}}<p class="error">{{.Error}}</p>{{end}}
|
||||
<span class="code">{{if .Output}}{{.Output}}{{else}}Queue is empty.{{end}}</span>
|
||||
</div>
|
||||
|
||||
@@ -44,7 +44,7 @@
|
||||
|
||||
{{/* The status page's section index, shown in the navigation column (see the
|
||||
"sections" block in layout.html). The first six cards are the ones the
|
||||
polling fragment replaces every five seconds; their ids are part of
|
||||
polling fragment replaces on a timer; their ids are part of
|
||||
status_body.html and do not change with the reading, so the links here hold
|
||||
across a refresh. */}}
|
||||
{{define "sections"}}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
{{define "status_body"}}
|
||||
<div id="status-body" hx-get="/status/fragment" hx-trigger="every 5s" hx-swap="outerHTML">
|
||||
<div id="status-body" data-poll hx-get="/status/fragment" hx-trigger="load" hx-swap="outerHTML">
|
||||
<div class="card" id="overall">
|
||||
<h2>Overall <span class="st st-{{.OverallStatus}}">{{.OverallStatus}}</span></h2>
|
||||
<p class="muted">{{.OverallHeading}}</p>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
{{define "system_log_body"}}
|
||||
<div id="system-log-body" hx-get="/system-log/body" hx-trigger="every 5s" hx-swap="outerHTML">
|
||||
<div id="system-log-body" data-poll hx-get="/system-log/body" hx-trigger="load" hx-swap="outerHTML">
|
||||
{{if .Error}}<p class="error">{{.Error}}</p>{{end}}
|
||||
<span class="code">{{if .Lines}}{{range .Lines}}{{.}}
|
||||
{{end}}{{else}}No log lines yet.{{end}}</span>
|
||||
|
||||
Reference in New Issue
Block a user