Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -7,6 +7,10 @@ Format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/); version
|
|||||||
|
|
||||||
### Changed
|
### Changed
|
||||||
|
|
||||||
|
- panel: the domain detail page is wide with paired cards (DKIM ‖ DNS status;
|
||||||
|
SPF ‖ DMARC; sending settings ‖ add application; rate limit ‖ export).
|
||||||
|
Applications and Danger stay full-width; SPF and DMARC are separate cards;
|
||||||
|
section blurbs are shorter.
|
||||||
- panel: page URLs, browser titles, and headings are aligned — **Settings** is
|
- panel: page URLs, browser titles, and headings are aligned — **Settings** is
|
||||||
now `/settings` (legacy `/account` redirects with 308); the domains list title
|
now `/settings` (legacy `/account` redirects with 308); the domains list title
|
||||||
is `SelfPost — domains`; Status, Users, and user create/edit titles match their
|
is `SelfPost — domains`; Status, Users, and user create/edit titles match their
|
||||||
|
|||||||
@@ -1,3 +1,7 @@
|
|||||||
|
{{/* Wide so the .split pairs fill the column rather than the 48rem reading
|
||||||
|
measure (same pattern as Status). */}}
|
||||||
|
{{define "wide"}}wide{{end}}
|
||||||
|
|
||||||
{{define "content"}}
|
{{define "content"}}
|
||||||
<h1>{{.Domain.Name}}</h1>
|
<h1>{{.Domain.Name}}</h1>
|
||||||
|
|
||||||
@@ -9,8 +13,8 @@
|
|||||||
{{if .NewCred}}
|
{{if .NewCred}}
|
||||||
<div class="card credential" id="new-credential">
|
<div class="card credential" id="new-credential">
|
||||||
<h2>New application password</h2>
|
<h2>New application password</h2>
|
||||||
<p class="muted">This password is shown <strong>once only</strong> and is not
|
<p class="muted">Shown <strong>once only</strong> and not stored. Copy it now
|
||||||
stored. Copy it now — if it is lost, regenerate a new one.</p>
|
— if it is lost, regenerate a new one.</p>
|
||||||
<label>Login</label>
|
<label>Login</label>
|
||||||
<div class="code-row">
|
<div class="code-row">
|
||||||
<span class="code">{{.NewCred.Login}}</span>
|
<span class="code">{{.NewCred.Login}}</span>
|
||||||
@@ -24,202 +28,197 @@
|
|||||||
</div>
|
</div>
|
||||||
{{end}}
|
{{end}}
|
||||||
|
|
||||||
<div class="card" id="dkim">
|
<div class="split">
|
||||||
<h2>DKIM DNS record</h2>
|
<div class="card" id="dkim">
|
||||||
<p class="muted">Publish this TXT record in the DNS for <strong>{{.Domain.Name}}</strong>.
|
<h2>DKIM DNS record</h2>
|
||||||
It is not a secret and can be viewed at any time.</p>
|
<p class="muted">Publish this TXT for <strong>{{.Domain.Name}}</strong>.
|
||||||
|
It is not a secret.</p>
|
||||||
|
|
||||||
<label>Host / name</label>
|
<label>Host / name</label>
|
||||||
<div class="code-row">
|
<div class="code-row">
|
||||||
<span class="code">{{.Record.Name}}</span>
|
<span class="code">{{.Record.Name}}</span>
|
||||||
<button type="button" class="copy">Copy</button>
|
<button type="button" class="copy">Copy</button>
|
||||||
</div>
|
|
||||||
|
|
||||||
<label>Type</label>
|
|
||||||
<span class="code">TXT</span>
|
|
||||||
|
|
||||||
<label>Value</label>
|
|
||||||
<div class="code-row">
|
|
||||||
<span class="code">{{.Record.Value}}</span>
|
|
||||||
<button type="button" class="copy">Copy</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<p class="muted">Mail is signed with selector <strong>{{.Domain.DKIMSelector}}</strong>.</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="card" id="spf-dmarc">
|
|
||||||
<h2>SPF and DMARC records</h2>
|
|
||||||
<p class="muted">These two are not generated the way the DKIM record above is —
|
|
||||||
they are policy, and the domain may already publish an SPF record for other
|
|
||||||
senders. SelfPost is a send-only relay — most operators have no inbox on the
|
|
||||||
sending domain, so the suggested DMARC record omits <code>rua=</code> unless a
|
|
||||||
report address is configured below or in <a href="/settings">Settings</a>.
|
|
||||||
Publish both as TXT records.</p>
|
|
||||||
|
|
||||||
<label>SPF — host / name</label>
|
|
||||||
<div class="code-row">
|
|
||||||
<span class="code">{{.Domain.Name}}</span>
|
|
||||||
<button type="button" class="copy">Copy</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<label>SPF — value</label>
|
|
||||||
<div class="code-row">
|
|
||||||
<span class="code">{{.SPFExample}}</span>
|
|
||||||
<button type="button" class="copy">Copy</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<p class="muted">A domain may have only one SPF record. If it already has one,
|
|
||||||
do not add a second — add this server's mechanism to the existing record
|
|
||||||
instead, before its <code>all</code> term.</p>
|
|
||||||
|
|
||||||
<form method="post" action="/domains/{{.Domain.ID}}/dmarc">
|
|
||||||
<label for="dmarc_rua_mode">DMARC aggregate reports (rua=)</label>
|
|
||||||
<select id="dmarc_rua_mode" name="dmarc_rua_mode">
|
|
||||||
<option value="inherit"{{if eq .DMARCRuaMode "inherit"}} selected{{end}}>Same as Settings{{if .ProfileDMARCEmail}} ({{.ProfileDMARCEmail}}){{end}}</option>
|
|
||||||
<option value="none"{{if eq .DMARCRuaMode "none"}} selected{{end}}>No aggregate reports</option>
|
|
||||||
<option value="custom"{{if eq .DMARCRuaMode "custom"}} selected{{end}}>Custom address</option>
|
|
||||||
</select>
|
|
||||||
|
|
||||||
<label for="dmarc_rua_email">Custom report address</label>
|
|
||||||
<input id="dmarc_rua_email" name="dmarc_rua_email" type="email"
|
|
||||||
autocapitalize="none" spellcheck="false" value="{{.DMARCRuaCustom}}"
|
|
||||||
placeholder="reports@your-mail-domain.com">
|
|
||||||
|
|
||||||
<button type="submit">Save DMARC report settings</button>
|
|
||||||
</form>
|
|
||||||
|
|
||||||
<label>DMARC — host / name</label>
|
|
||||||
<div class="code-row">
|
|
||||||
<span class="code">{{.DMARCName}}</span>
|
|
||||||
<button type="button" class="copy">Copy</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<label>DMARC — value{{if eq .DMARCSource "settings"}} <span class="muted">(from Settings)</span>{{else if eq .DMARCSource "custom"}} <span class="muted">(custom)</span>{{else if eq .DMARCSource "none"}} <span class="muted">(no reports)</span>{{end}}</label>
|
|
||||||
<div class="code-row">
|
|
||||||
<span class="code">{{.DMARCExample}}</span>
|
|
||||||
<button type="button" class="copy">Copy</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{{if .SameDomainRUA}}
|
|
||||||
<p class="error">The report address is on this sending domain. SelfPost does
|
|
||||||
not receive inbound mail — use a mailbox elsewhere or wait for in-panel report
|
|
||||||
reception in a future release.</p>
|
|
||||||
{{end}}
|
|
||||||
|
|
||||||
{{if .NeedsReportAuth}}
|
|
||||||
<label>Report authorization — host / name</label>
|
|
||||||
<div class="code-row">
|
|
||||||
<span class="code">{{.ReportAuthName}}</span>
|
|
||||||
<button type="button" class="copy">Copy</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<label>Report authorization — value</label>
|
|
||||||
<div class="code-row">
|
|
||||||
<span class="code">{{.ReportAuthValue}}</span>
|
|
||||||
<button type="button" class="copy">Copy</button>
|
|
||||||
</div>
|
|
||||||
{{end}}
|
|
||||||
|
|
||||||
<p class="muted"><code>p=none</code> changes nothing about delivery. Omit
|
|
||||||
<code>rua=</code> on a send-only domain, or point it at a mailbox that
|
|
||||||
receives inbound mail. Tighten to <code>p=quarantine</code> and then
|
|
||||||
<code>p=reject</code> once aggregate reports look clean.</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="card" id="dns-status">
|
|
||||||
<h2>DNS status <span class="st st-{{.DNS.Overall}}">{{.DNS.Overall}}</span></h2>
|
|
||||||
<p class="muted">What DNS publishes for <strong>{{.Domain.Name}}</strong> right
|
|
||||||
now, checked against the key this server signs with. Results are cached for a
|
|
||||||
few minutes — after publishing a record, use <em>Re-check</em>.</p>
|
|
||||||
|
|
||||||
<label>DKIM <span class="st st-{{.DNS.DKIM.Status}}">{{.DNS.DKIM.Status}}</span></label>
|
|
||||||
<p class="{{if eq .DNS.DKIM.Status "ok"}}muted{{else}}error{{end}}">{{.DNS.DKIM.Detail}}</p>
|
|
||||||
{{if .DNS.DKIM.Records}}<span class="code">{{range .DNS.DKIM.Records}}{{.}}
|
|
||||||
{{end}}</span>{{end}}
|
|
||||||
|
|
||||||
<label>SPF <span class="st st-{{.DNS.SPF.Status}}">{{.DNS.SPF.Status}}</span></label>
|
|
||||||
<p class="{{if eq .DNS.SPF.Status "ok"}}muted{{else}}error{{end}}">{{.DNS.SPF.Detail}}</p>
|
|
||||||
{{if .DNS.SPF.Records}}<span class="code">{{range .DNS.SPF.Records}}{{.}}
|
|
||||||
{{end}}</span>{{end}}
|
|
||||||
<p class="muted">The SPF check is deliberately shallow: it looks for a
|
|
||||||
mechanism that literally covers this server's address and does not follow
|
|
||||||
<code>include:</code> or <code>redirect=</code>, so a record that authorises
|
|
||||||
the server through an include is reported as “cannot tell”, not as a failure.</p>
|
|
||||||
|
|
||||||
<label>DMARC <span class="st st-{{.DNS.DMARC.Status}}">{{.DNS.DMARC.Status}}</span></label>
|
|
||||||
<p class="{{if eq .DNS.DMARC.Status "ok"}}muted{{else}}error{{end}}">{{.DNS.DMARC.Detail}}</p>
|
|
||||||
{{if .DNS.DMARC.Records}}<span class="code">{{range .DNS.DMARC.Records}}{{.}}
|
|
||||||
{{end}}</span>{{end}}
|
|
||||||
|
|
||||||
{{if .DNS.DMARCReportAuth.Status}}
|
|
||||||
<label>Report authorization <span class="st st-{{.DNS.DMARCReportAuth.Status}}">{{.DNS.DMARCReportAuth.Status}}</span></label>
|
|
||||||
<p class="{{if eq .DNS.DMARCReportAuth.Status "ok"}}muted{{else}}error{{end}}">{{.DNS.DMARCReportAuth.Detail}}</p>
|
|
||||||
{{if .DNS.DMARCReportAuth.Records}}<span class="code">{{range .DNS.DMARCReportAuth.Records}}{{.}}
|
|
||||||
{{end}}</span>{{end}}
|
|
||||||
{{end}}
|
|
||||||
|
|
||||||
<form class="inline" method="post" action="/domains/{{.Domain.ID}}/dns-recheck">
|
|
||||||
<button type="submit">Re-check</button>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="card" id="settings">
|
|
||||||
<h2>Sending server settings</h2>
|
|
||||||
<p class="muted">Point the mail client or script at these settings and
|
|
||||||
authenticate with an application login and password from the
|
|
||||||
<strong>Applications</strong> section below. They are the same for every
|
|
||||||
domain on this server.</p>
|
|
||||||
|
|
||||||
<label>Server</label>
|
|
||||||
<div class="code-row">
|
|
||||||
<span class="code">{{.Hostname}}</span>
|
|
||||||
<button type="button" class="copy">Copy</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<label>Port and encryption</label>
|
|
||||||
<span class="code">465 — SSL/TLS (implicit){{if .SubmissionEnabled}}
|
|
||||||
587 — STARTTLS (submission){{end}}</span>
|
|
||||||
|
|
||||||
<p class="muted">Authentication is required on every port. The username is the
|
|
||||||
application's login (see the table below) and the password is the one shown
|
|
||||||
once when that application was created or its password regenerated — if it was
|
|
||||||
lost, generate a new one.</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{{/* Create form above the list, the same order the domains page uses for
|
|
||||||
"Add a sending domain" above "Domains". */}}
|
|
||||||
<div class="card" id="add-application">
|
|
||||||
<h2>Add an application</h2>
|
|
||||||
<form method="post" action="/domains/{{.Domain.ID}}/applications">
|
|
||||||
<label for="login">Login</label>
|
|
||||||
<input id="login" name="login" type="text" placeholder="prod-server"
|
|
||||||
autocomplete="off" autocapitalize="none" spellcheck="false"
|
|
||||||
value="{{.FormLogin}}" required>
|
|
||||||
|
|
||||||
<label for="mode">Address mode</label>
|
|
||||||
<select id="mode" name="mode" data-list-mode="{{.List}}">
|
|
||||||
<option value="{{.Wildcard}}" {{if eq .FormMode .Wildcard}}selected{{end}}>Any address of the domain</option>
|
|
||||||
<option value="{{.List}}" {{if eq .FormMode .List}}selected{{end}}>Specific addresses (list)</option>
|
|
||||||
</select>
|
|
||||||
|
|
||||||
<div data-addresses>
|
|
||||||
<label for="addresses">Addresses (one per line or comma-separated)</label>
|
|
||||||
<textarea id="addresses" name="addresses" rows="3"
|
|
||||||
placeholder="alerts@{{.Domain.Name}}">{{.FormAddrs}}</textarea>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{{if .Error}}<p class="error">{{.Error}}</p>{{end}}
|
<label>Type</label>
|
||||||
<button type="submit">Create application</button>
|
<span class="code">TXT</span>
|
||||||
</form>
|
|
||||||
<p class="muted">A strong password is generated and shown once. The login must
|
<label>Value</label>
|
||||||
be unique across all domains and may contain letters, digits, '.', '-' and '_'.</p>
|
<div class="code-row">
|
||||||
|
<span class="code">{{.Record.Value}}</span>
|
||||||
|
<button type="button" class="copy">Copy</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p class="muted">Signed with selector <strong>{{.Domain.DKIMSelector}}</strong>.</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card" id="dns-status">
|
||||||
|
<h2>DNS status <span class="st st-{{.DNS.Overall}}">{{.DNS.Overall}}</span></h2>
|
||||||
|
<p class="muted">Cached a few minutes — use <em>Re-check</em> after
|
||||||
|
publishing.</p>
|
||||||
|
|
||||||
|
<label>DKIM <span class="st st-{{.DNS.DKIM.Status}}">{{.DNS.DKIM.Status}}</span></label>
|
||||||
|
<p class="{{if eq .DNS.DKIM.Status "ok"}}muted{{else}}error{{end}}">{{.DNS.DKIM.Detail}}</p>
|
||||||
|
{{if .DNS.DKIM.Records}}<span class="code">{{range .DNS.DKIM.Records}}{{.}}
|
||||||
|
{{end}}</span>{{end}}
|
||||||
|
|
||||||
|
<label>SPF <span class="st st-{{.DNS.SPF.Status}}">{{.DNS.SPF.Status}}</span></label>
|
||||||
|
<p class="{{if eq .DNS.SPF.Status "ok"}}muted{{else}}error{{end}}">{{.DNS.SPF.Detail}}</p>
|
||||||
|
{{if .DNS.SPF.Records}}<span class="code">{{range .DNS.SPF.Records}}{{.}}
|
||||||
|
{{end}}</span>{{end}}
|
||||||
|
<p class="muted">Shallow check: literal address only, no
|
||||||
|
<code>include:</code> / <code>redirect=</code>.</p>
|
||||||
|
|
||||||
|
<label>DMARC <span class="st st-{{.DNS.DMARC.Status}}">{{.DNS.DMARC.Status}}</span></label>
|
||||||
|
<p class="{{if eq .DNS.DMARC.Status "ok"}}muted{{else}}error{{end}}">{{.DNS.DMARC.Detail}}</p>
|
||||||
|
{{if .DNS.DMARC.Records}}<span class="code">{{range .DNS.DMARC.Records}}{{.}}
|
||||||
|
{{end}}</span>{{end}}
|
||||||
|
|
||||||
|
{{if .DNS.DMARCReportAuth.Status}}
|
||||||
|
<label>Report authorization <span class="st st-{{.DNS.DMARCReportAuth.Status}}">{{.DNS.DMARCReportAuth.Status}}</span></label>
|
||||||
|
<p class="{{if eq .DNS.DMARCReportAuth.Status "ok"}}muted{{else}}error{{end}}">{{.DNS.DMARCReportAuth.Detail}}</p>
|
||||||
|
{{if .DNS.DMARCReportAuth.Records}}<span class="code">{{range .DNS.DMARCReportAuth.Records}}{{.}}
|
||||||
|
{{end}}</span>{{end}}
|
||||||
|
{{end}}
|
||||||
|
|
||||||
|
<form class="inline" method="post" action="/domains/{{.Domain.ID}}/dns-recheck">
|
||||||
|
<button type="submit">Re-check</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="split">
|
||||||
|
<div class="card" id="spf">
|
||||||
|
<h2>SPF record</h2>
|
||||||
|
<p class="muted">Suggested TXT. If the domain already has SPF, merge this
|
||||||
|
server's mechanism into it — do not add a second record.</p>
|
||||||
|
|
||||||
|
<label>Host / name</label>
|
||||||
|
<div class="code-row">
|
||||||
|
<span class="code">{{.Domain.Name}}</span>
|
||||||
|
<button type="button" class="copy">Copy</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<label>Value</label>
|
||||||
|
<div class="code-row">
|
||||||
|
<span class="code">{{.SPFExample}}</span>
|
||||||
|
<button type="button" class="copy">Copy</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card" id="dmarc">
|
||||||
|
<h2>DMARC record</h2>
|
||||||
|
<p class="muted">Policy TXT. SelfPost is send-only — omit <code>rua=</code>
|
||||||
|
or point it at a mailbox elsewhere (<a href="/settings">Settings</a> default
|
||||||
|
or custom below).</p>
|
||||||
|
|
||||||
|
<form method="post" action="/domains/{{.Domain.ID}}/dmarc">
|
||||||
|
<label for="dmarc_rua_mode">Aggregate reports (rua=)</label>
|
||||||
|
<select id="dmarc_rua_mode" name="dmarc_rua_mode">
|
||||||
|
<option value="inherit"{{if eq .DMARCRuaMode "inherit"}} selected{{end}}>Same as Settings{{if .ProfileDMARCEmail}} ({{.ProfileDMARCEmail}}){{end}}</option>
|
||||||
|
<option value="none"{{if eq .DMARCRuaMode "none"}} selected{{end}}>No aggregate reports</option>
|
||||||
|
<option value="custom"{{if eq .DMARCRuaMode "custom"}} selected{{end}}>Custom address</option>
|
||||||
|
</select>
|
||||||
|
|
||||||
|
<label for="dmarc_rua_email">Custom report address</label>
|
||||||
|
<input id="dmarc_rua_email" name="dmarc_rua_email" type="email"
|
||||||
|
autocapitalize="none" spellcheck="false" value="{{.DMARCRuaCustom}}"
|
||||||
|
placeholder="reports@your-mail-domain.com">
|
||||||
|
|
||||||
|
<button type="submit">Save DMARC report settings</button>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<label>Host / name</label>
|
||||||
|
<div class="code-row">
|
||||||
|
<span class="code">{{.DMARCName}}</span>
|
||||||
|
<button type="button" class="copy">Copy</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<label>Value{{if eq .DMARCSource "settings"}} <span class="muted">(from Settings)</span>{{else if eq .DMARCSource "custom"}} <span class="muted">(custom)</span>{{else if eq .DMARCSource "none"}} <span class="muted">(no reports)</span>{{end}}</label>
|
||||||
|
<div class="code-row">
|
||||||
|
<span class="code">{{.DMARCExample}}</span>
|
||||||
|
<button type="button" class="copy">Copy</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{{if .SameDomainRUA}}
|
||||||
|
<p class="error">The report address is on this sending domain. SelfPost does
|
||||||
|
not receive inbound mail — use a mailbox elsewhere or wait for in-panel report
|
||||||
|
reception in a future release.</p>
|
||||||
|
{{end}}
|
||||||
|
|
||||||
|
{{if .NeedsReportAuth}}
|
||||||
|
<label>Report authorization — host / name</label>
|
||||||
|
<div class="code-row">
|
||||||
|
<span class="code">{{.ReportAuthName}}</span>
|
||||||
|
<button type="button" class="copy">Copy</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<label>Report authorization — value</label>
|
||||||
|
<div class="code-row">
|
||||||
|
<span class="code">{{.ReportAuthValue}}</span>
|
||||||
|
<button type="button" class="copy">Copy</button>
|
||||||
|
</div>
|
||||||
|
{{end}}
|
||||||
|
|
||||||
|
<p class="muted"><code>p=none</code> does not affect delivery. Tighten to
|
||||||
|
<code>p=quarantine</code> then <code>p=reject</code> once reports look clean.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="split">
|
||||||
|
<div class="card" id="settings">
|
||||||
|
<h2>Sending server settings</h2>
|
||||||
|
<p class="muted">Same for every domain. Authenticate with an application
|
||||||
|
login from below.</p>
|
||||||
|
|
||||||
|
<label>Server</label>
|
||||||
|
<div class="code-row">
|
||||||
|
<span class="code">{{.Hostname}}</span>
|
||||||
|
<button type="button" class="copy">Copy</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<label>Port and encryption</label>
|
||||||
|
<span class="code">465 — SSL/TLS (implicit){{if .SubmissionEnabled}}
|
||||||
|
587 — STARTTLS (submission){{end}}</span>
|
||||||
|
|
||||||
|
<p class="muted">Auth required on every port. The password is shown once at
|
||||||
|
create or regenerate.</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{{/* Create form beside settings, mirroring "Add a sending domain" above the
|
||||||
|
domains list. */}}
|
||||||
|
<div class="card" id="add-application">
|
||||||
|
<h2>Add an application</h2>
|
||||||
|
<form method="post" action="/domains/{{.Domain.ID}}/applications">
|
||||||
|
<label for="login">Login</label>
|
||||||
|
<input id="login" name="login" type="text" placeholder="prod-server"
|
||||||
|
autocomplete="off" autocapitalize="none" spellcheck="false"
|
||||||
|
value="{{.FormLogin}}" required>
|
||||||
|
|
||||||
|
<label for="mode">Address mode</label>
|
||||||
|
<select id="mode" name="mode" data-list-mode="{{.List}}">
|
||||||
|
<option value="{{.Wildcard}}" {{if eq .FormMode .Wildcard}}selected{{end}}>Any address of the domain</option>
|
||||||
|
<option value="{{.List}}" {{if eq .FormMode .List}}selected{{end}}>Specific addresses (list)</option>
|
||||||
|
</select>
|
||||||
|
|
||||||
|
<div data-addresses>
|
||||||
|
<label for="addresses">Addresses (one per line or comma-separated)</label>
|
||||||
|
<textarea id="addresses" name="addresses" rows="3"
|
||||||
|
placeholder="alerts@{{.Domain.Name}}">{{.FormAddrs}}</textarea>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{{if .Error}}<p class="error">{{.Error}}</p>{{end}}
|
||||||
|
<button type="submit">Create application</button>
|
||||||
|
</form>
|
||||||
|
<p class="muted">Password shown once. Login unique across domains; letters,
|
||||||
|
digits, '.', '-' and '_'.</p>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="card" id="applications">
|
<div class="card" id="applications">
|
||||||
<h2>Applications</h2>
|
<h2>Applications</h2>
|
||||||
<p class="muted">Each application is a SASL login/password an app or script
|
<p class="muted">SASL logins for this domain — wildcard (*@domain) or a fixed
|
||||||
uses to send mail as this domain. A login may send from any address of the
|
address list.</p>
|
||||||
domain (<em>wildcard</em>) or only from a fixed list of addresses.</p>
|
|
||||||
|
|
||||||
{{if .Apps}}
|
{{if .Apps}}
|
||||||
<ul class="apps">
|
<ul class="apps">
|
||||||
@@ -298,79 +297,69 @@
|
|||||||
{{end}}
|
{{end}}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="card" id="rate-limit">
|
<div class="split">
|
||||||
<h2>Sending rate limit (domain)</h2>
|
<div class="card" id="rate-limit">
|
||||||
<p class="muted">Optional level-2 limit (spec 7.4): cap how many messages this
|
<h2>Sending rate limit (domain)</h2>
|
||||||
domain may send from its expected client IP(s) within a time window, summed
|
<p class="muted">Optional level-2 cap across this domain's applications from
|
||||||
across all its applications. It counts messages — one message to many
|
the listed client IPs. Empty IP list = inactive (level-1 only).</p>
|
||||||
recipients counts once. Leave the IP list empty to disable it and rely only on
|
|
||||||
the global level-1 limit. Applications that send from changing IPs should be
|
|
||||||
left unbound here.</p>
|
|
||||||
|
|
||||||
<p class="muted">Status:
|
<p class="muted">Status:
|
||||||
{{if .DomainHasRL}}<strong>active</strong>{{else}}inactive (level-1 only){{end}}.</p>
|
{{if .DomainHasRL}}<strong>active</strong>{{else}}inactive (level-1 only){{end}}.</p>
|
||||||
|
|
||||||
<form method="post" action="/domains/{{.Domain.ID}}/ratelimit">
|
<form method="post" action="/domains/{{.Domain.ID}}/ratelimit">
|
||||||
<label for="d_ips">Expected client IPs (one per line or comma-separated)</label>
|
<label for="d_ips">Expected client IPs (one per line or comma-separated)</label>
|
||||||
<textarea id="d_ips" name="allowed_ips" rows="2"
|
<textarea id="d_ips" name="allowed_ips" rows="2"
|
||||||
placeholder="203.0.113.10">{{.DomainRLIPs}}</textarea>
|
placeholder="203.0.113.10">{{.DomainRLIPs}}</textarea>
|
||||||
|
|
||||||
<label for="d_max">Message limit</label>
|
<label for="d_max">Message limit</label>
|
||||||
<input id="d_max" name="max_messages" type="number" min="1"
|
<input id="d_max" name="max_messages" type="number" min="1"
|
||||||
value="{{.DomainRLMax}}" placeholder="1000">
|
value="{{.DomainRLMax}}" placeholder="1000">
|
||||||
|
|
||||||
<label for="d_win">Window (seconds)</label>
|
<label for="d_win">Window (seconds)</label>
|
||||||
<input id="d_win" name="window_seconds" type="number" min="1" value="{{.DomainRLWin}}">
|
<input id="d_win" name="window_seconds" type="number" min="1" value="{{.DomainRLWin}}">
|
||||||
|
|
||||||
<button type="submit">Save limit</button>
|
<button type="submit">Save limit</button>
|
||||||
</form>
|
</form>
|
||||||
{{if .DomainHasRL}}
|
{{if .DomainHasRL}}
|
||||||
<form class="inline" method="post" action="/domains/{{.Domain.ID}}/ratelimit"
|
<form class="inline" method="post" action="/domains/{{.Domain.ID}}/ratelimit"
|
||||||
data-confirm="Remove the domain rate limit? Only the global level-1 limit will apply.">
|
data-confirm="Remove the domain rate limit? Only the global level-1 limit will apply.">
|
||||||
<input type="hidden" name="clear" value="1">
|
<input type="hidden" name="clear" value="1">
|
||||||
<button type="submit" class="danger">Remove limit</button>
|
<button type="submit" class="danger">Remove limit</button>
|
||||||
</form>
|
</form>
|
||||||
{{end}}
|
{{end}}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="card" id="export">
|
<div class="card" id="export">
|
||||||
<h2>Export domain</h2>
|
<h2>Export domain</h2>
|
||||||
<p class="muted">Download this domain to move it to another SelfPost instance:
|
<p class="muted">DKIM key, selector and application passwords for another
|
||||||
its DKIM key, selector and every application with its working password. On
|
SelfPost instance. DNS stays the same on import.</p>
|
||||||
import the DNS record stays the same, so no DNS change is needed.</p>
|
<p class="muted"><strong>Secret file</strong> — transfer securely, or encrypt
|
||||||
<p class="muted"><strong>The export file is a secret</strong> — it contains the
|
below as <code>.spde</code>.</p>
|
||||||
private DKIM key and application passwords. Transfer it securely and delete it
|
{{if .ExportErr}}<p class="error">{{.ExportErr}}</p>{{end}}
|
||||||
after the import, or encrypt it below and move a <code>.spde</code> file
|
<form method="post" action="/domains/{{.Domain.ID}}/export">
|
||||||
(SelfPost domain export) instead; the import form asks for the password.</p>
|
{{template "encryptfields" .}}
|
||||||
{{if .ExportErr}}<p class="error">{{.ExportErr}}</p>{{end}}
|
<button type="submit">Export domain</button>
|
||||||
<form method="post" action="/domains/{{.Domain.ID}}/export">
|
</form>
|
||||||
{{template "encryptfields" .}}
|
</div>
|
||||||
<button type="submit">Export domain</button>
|
|
||||||
</form>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="card" id="danger">
|
<div class="card" id="danger">
|
||||||
<h2>Danger zone</h2>
|
<h2>Danger zone</h2>
|
||||||
<p class="muted">Deleting this domain also deletes its DKIM key and every
|
<p class="muted">Deletes the DKIM key and every application on this domain.</p>
|
||||||
application bound to it.</p>
|
|
||||||
<a class="danger" href="/domains/{{.Domain.ID}}/delete">Delete domain</a>
|
<a class="danger" href="/domains/{{.Domain.ID}}/delete">Delete domain</a>
|
||||||
</div>
|
</div>
|
||||||
{{end}}
|
{{end}}
|
||||||
|
|
||||||
{{/* The domain page's section index, shown in the navigation column (see the
|
{{/* Section index in the navigation column (layout.html). The freshly generated
|
||||||
"sections" block in layout.html). This is the panel's longest page — the
|
password is only listed while it is on the page. */}}
|
||||||
DNS records to publish, the checks on them, the applications and two rate
|
|
||||||
limits — and setting a domain up means going back and forth between them.
|
|
||||||
The freshly generated password is only listed while it is on the page: it
|
|
||||||
is the one card that is not always there, and the one nothing should scroll
|
|
||||||
away from silently. */}}
|
|
||||||
{{define "sections"}}
|
{{define "sections"}}
|
||||||
<div class="sections">
|
<div class="sections">
|
||||||
<p class="sections-title">On this page</p>
|
<p class="sections-title">On this page</p>
|
||||||
{{if .NewCred}}<a href="#new-credential">New application password</a>{{end}}
|
{{if .NewCred}}<a href="#new-credential">New application password</a>{{end}}
|
||||||
<a href="#dkim">DKIM DNS record</a>
|
<a href="#dkim">DKIM DNS record</a>
|
||||||
<a href="#spf-dmarc">SPF and DMARC records</a>
|
|
||||||
<a href="#dns-status">DNS status</a>
|
<a href="#dns-status">DNS status</a>
|
||||||
|
<a href="#spf">SPF record</a>
|
||||||
|
<a href="#dmarc">DMARC record</a>
|
||||||
<a href="#settings">Sending server settings</a>
|
<a href="#settings">Sending server settings</a>
|
||||||
<a href="#add-application">Add an application</a>
|
<a href="#add-application">Add an application</a>
|
||||||
<a href="#applications">Applications</a>
|
<a href="#applications">Applications</a>
|
||||||
|
|||||||
@@ -264,7 +264,7 @@ func TestOnlyThePagesMadeOfDataDeclareThemselvesWide(t *testing.T) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("New: %v", err)
|
t.Fatalf("New: %v", err)
|
||||||
}
|
}
|
||||||
wide := map[string]bool{"settings": true, "deliveries": true, "delivery": true, "mail_queue": true, "status": true, "system_log": true}
|
wide := map[string]bool{"settings": true, "deliveries": true, "delivery": true, "mail_queue": true, "status": true, "system_log": true, "domain_detail": true}
|
||||||
for name, page := range engine.Pages() {
|
for name, page := range engine.Pages() {
|
||||||
var buf bytes.Buffer
|
var buf bytes.Buffer
|
||||||
if err := page.ExecuteTemplate(&buf, "wide", nil); err != nil {
|
if err := page.ExecuteTemplate(&buf, "wide", nil); err != nil {
|
||||||
@@ -280,6 +280,32 @@ func TestOnlyThePagesMadeOfDataDeclareThemselvesWide(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// The domain page pairs cards the same way Status does: four .split rows
|
||||||
|
// (DKIM|DNS, SPF|DMARC, settings|add-app, rate-limit|export) with Applications
|
||||||
|
// and Danger full-width. Losing a row silently stacks the page again.
|
||||||
|
func TestDomainDetailPageHasPairedCards(t *testing.T) {
|
||||||
|
body, err := fs.ReadFile(assetsFS, "templates/domain_detail.html")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("read domain_detail: %v", err)
|
||||||
|
}
|
||||||
|
src := string(body)
|
||||||
|
if got := strings.Count(src, `class="split"`); got != 4 {
|
||||||
|
t.Errorf("domain detail has %d .split rows, want 4", got)
|
||||||
|
}
|
||||||
|
for _, id := range []string{
|
||||||
|
`id="dkim"`, `id="dns-status"`, `id="spf"`, `id="dmarc"`,
|
||||||
|
`id="settings"`, `id="add-application"`, `id="applications"`,
|
||||||
|
`id="rate-limit"`, `id="export"`, `id="danger"`,
|
||||||
|
} {
|
||||||
|
if !strings.Contains(src, id) {
|
||||||
|
t.Errorf("domain detail is missing %s", id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if strings.Contains(src, `id="spf-dmarc"`) {
|
||||||
|
t.Error("domain detail still has the combined spf-dmarc card; SPF and DMARC are separate")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Drill-down pages carry an up-link directly under the heading and above the
|
// Drill-down pages carry an up-link directly under the heading and above the
|
||||||
// cards. A link at the bottom of a form is easy to miss and drifts from the
|
// cards. A link at the bottom of a form is easy to miss and drifts from the
|
||||||
// rest of the panel, so the shared back_link template is mandatory on those
|
// rest of the panel, so the shared back_link template is mandatory on those
|
||||||
|
|||||||
Reference in New Issue
Block a user