test / test (push) Has been cancelled
Review pass over the 1.4.0-1.9.1 work. Port 25 no longer inherits main.cf's submission milter chain: smtpd_milters is always overridden on smtp/inet, to the antispam milter or to nothing. Without that, OpenDKIM (strict, tempfail) gated relayed mail during a signing outage, and the journal-milter filed inbound mail into the outbound send log under the sender's domain -- so Deliveries showed other people's mail and a forged From: consumed that domain's level-2 budget. architecture.md already described the intended behaviour; the code now matches. DMARC reports are checked before they are stored: the policy domain must be a configured sending domain, and must match the +tag of a per-domain hosted address. The rua address form is predictable, so anyone able to reach it could file reports under any domain and evict genuine ones through the retention cap. Zip attachments are now decompressed as well as gzip. The auto level-2 ceiling is scaled to the level-1 window before the multiplier is applied; any window other than 3600s shifted the allowance by window/3600. Migration 0010 indexes send_log(app_login, created_at) -- both the per-MAIL FROM rate-limit count and the per-application statistics were scanning the created_at range. e2e now regenerates the Postfix config in-container for all four port-25 flag combinations and requires postfix check to pass; the 1.9.1 crash-loop shipped in 1.4.0 and survived three releases because only the flags-off path was exercised. Migration 0009 stays as shipped -- its conversion branch only fires for a database carrying application-scope allowed_ips from before 1.9.0. The schema reference notes that the old list was permissive and the new column is restrictive; the guide documents the allow-list as a sending control on a fail-open path, not an authentication boundary. Also: gofmt across cmd/ and internal/, unused duplicate validators dropped from internal/web/validate, export no longer swallows store errors. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
524 lines
23 KiB
HTML
524 lines
23 KiB
HTML
{{/* Wide so the .split pairs fill the column rather than the 48rem reading
|
||
measure (same pattern as Status). */}}
|
||
{{define "wide"}}wide{{end}}
|
||
|
||
{{/* The Host/name ‖ Type field-pair repeats for every DNS record this page
|
||
shows (DKIM, SPF, DMARC, report authorization) in both the status card
|
||
and the publishable-record cards below it — only the host and whether it
|
||
carries a Copy button change. Two variants rather than one templated
|
||
Copy flag: the DNS status card never offers Copy (its host is derived,
|
||
not something to paste), the record cards always do. */}}
|
||
{{define "host_type"}}
|
||
<div class="field-pair host-type">
|
||
<div>
|
||
<label>Host / name</label>
|
||
<span class="code">{{.}}</span>
|
||
</div>
|
||
<div class="field-type">
|
||
<label>Type</label>
|
||
<span class="code">TXT</span>
|
||
</div>
|
||
</div>
|
||
{{end}}
|
||
|
||
{{define "host_type_copy"}}
|
||
<div class="field-pair host-type">
|
||
<div>
|
||
<label>Host / name</label>
|
||
<div class="code-row">
|
||
<span class="code">{{.}}</span>
|
||
<button type="button" class="copy">Copy</button>
|
||
</div>
|
||
</div>
|
||
<div class="field-type">
|
||
<label>Type</label>
|
||
<span class="code">TXT</span>
|
||
</div>
|
||
</div>
|
||
{{end}}
|
||
|
||
{{/* Value partials pair with the two host_type variants above: field_values
|
||
is the DNS status card's raw record dump (no Copy — those values are for
|
||
comparison, not for pasting), field_value is the single publishable
|
||
value on the record cards below (always has Copy). Callers still guard
|
||
the empty case, since "no records yet" and "one blank record" read
|
||
differently. */}}
|
||
{{define "field_values"}}
|
||
<label>Value</label>
|
||
<span class="code">{{range .}}{{.}}
|
||
{{end}}</span>
|
||
{{end}}
|
||
|
||
{{define "field_value"}}
|
||
<label>Value</label>
|
||
<div class="code-row">
|
||
<span class="code">{{.}}</span>
|
||
<button type="button" class="copy">Copy</button>
|
||
</div>
|
||
{{end}}
|
||
|
||
{{define "content"}}
|
||
<h1>{{.Domain.Name}}</h1>
|
||
|
||
{{template "back_link" (back "/domains" "All domains")}}
|
||
|
||
{{if .Flash}}<div class="flash">{{.Flash}}</div>{{end}}
|
||
{{if .RateLimitErr}}<div class="flash error">{{.RateLimitErr}}</div>{{end}}
|
||
|
||
{{if .NewCred}}
|
||
<div class="card credential" id="new-credential">
|
||
<div class="card-head">
|
||
<h2>New application password</h2>
|
||
{{template "card_help" (dict "ID" "password" "Title" "Why this is shown once")}}
|
||
</div>
|
||
<p class="muted">Shown <strong>once only</strong> and not stored. Copy it now
|
||
— if it is lost, regenerate a new one.</p>
|
||
<label>Login</label>
|
||
<div class="code-row">
|
||
<span class="code">{{.NewCred.Login}}</span>
|
||
<button type="button" class="copy">Copy</button>
|
||
</div>
|
||
<label>Password</label>
|
||
<div class="code-row">
|
||
<span class="code">{{.NewCred.Password}}</span>
|
||
<button type="button" class="copy">Copy</button>
|
||
</div>
|
||
</div>
|
||
{{end}}
|
||
|
||
{{/* Two rows of two checks (.check-cols). */}}
|
||
<div class="card" id="dns-status">
|
||
<div class="card-head">
|
||
<h2>DNS status <span class="st st-{{.DNS.Overall}}">{{.DNS.Overall}}</span></h2>
|
||
{{template "card_help" (dict "ID" "dns" "Title" "What these checks mean")}}
|
||
</div>
|
||
<p class="muted">Cached a few minutes — use <em>Re-check</em> after
|
||
publishing.</p>
|
||
|
||
<div class="check-cols">
|
||
<div class="check-col">
|
||
<label>DKIM <span class="st st-{{.DNS.DKIM.Status}}">{{.DNS.DKIM.Status}}</span></label>
|
||
{{template "host_type" .Record.Name}}
|
||
{{if .DNS.DKIM.Records}}{{template "field_values" .DNS.DKIM.Records}}{{end}}
|
||
{{if ne .DNS.DKIM.Status "ok"}}
|
||
<p class="{{if eq .DNS.DKIM.Status "unknown"}}muted{{else}}error{{end}}">{{.DNS.DKIM.Detail}}</p>
|
||
{{end}}
|
||
</div>
|
||
|
||
<div class="check-col">
|
||
<label>SPF <span class="st st-{{.DNS.SPF.Status}}">{{.DNS.SPF.Status}}</span></label>
|
||
{{template "host_type" .Domain.Name}}
|
||
{{if .DNS.SPF.Records}}{{template "field_values" .DNS.SPF.Records}}{{end}}
|
||
{{if ne .DNS.SPF.Status "ok"}}
|
||
<p class="{{if eq .DNS.SPF.Status "unknown"}}muted{{else}}error{{end}}">{{.DNS.SPF.Detail}}</p>
|
||
{{end}}
|
||
<p class="muted">Shallow check: literal address only, no <code>include:</code> /
|
||
<code>redirect=</code>.</p>
|
||
</div>
|
||
|
||
<div class="check-col">
|
||
<label>DMARC <span class="st st-{{.DNS.DMARC.Status}}">{{.DNS.DMARC.Status}}</span></label>
|
||
{{template "host_type" .DMARCName}}
|
||
{{if .DNS.DMARC.Records}}{{template "field_values" .DNS.DMARC.Records}}{{end}}
|
||
{{if ne .DNS.DMARC.Status "ok"}}
|
||
<p class="{{if eq .DNS.DMARC.Status "unknown"}}muted{{else}}error{{end}}">{{.DNS.DMARC.Detail}}</p>
|
||
{{else}}
|
||
<p class="muted">{{.DNS.DMARC.Detail}}</p>
|
||
{{end}}
|
||
</div>
|
||
|
||
<div class="check-col">
|
||
{{if .DNS.DMARCReportAuth.Status}}
|
||
<label>Report authorization <span class="st st-{{.DNS.DMARCReportAuth.Status}}">{{.DNS.DMARCReportAuth.Status}}</span></label>
|
||
{{template "host_type" .ReportAuthName}}
|
||
{{if .DNS.DMARCReportAuth.Records}}{{template "field_values" .DNS.DMARCReportAuth.Records}}{{end}}
|
||
<p class="{{if eq .DNS.DMARCReportAuth.Status "ok"}}muted{{else}}error{{end}}">{{.DNS.DMARCReportAuth.Detail}}</p>
|
||
{{else}}
|
||
<label>Report authorization</label>
|
||
<p class="muted">Not required (no external <code>rua=</code>).</p>
|
||
{{end}}
|
||
</div>
|
||
</div>
|
||
|
||
<form class="inline" method="post" action="/domains/{{.Domain.ID}}/dns-recheck">
|
||
<button type="submit">Re-check</button>
|
||
</form>
|
||
</div>
|
||
|
||
<div class="split">
|
||
<div class="card" id="dkim-spf">
|
||
<div class="card-head">
|
||
<h2>DKIM and SPF records</h2>
|
||
{{template "card_help" (dict "ID" "records" "Title" "What to publish")}}
|
||
</div>
|
||
|
||
<p class="check-col-title">DKIM</p>
|
||
{{template "host_type_copy" .Record.Name}}
|
||
{{template "field_value" .Record.Value}}
|
||
|
||
<p class="muted">Not a secret. Signed with selector
|
||
<strong>{{.Domain.DKIMSelector}}</strong>.</p>
|
||
|
||
<p class="check-col-title">SPF</p>
|
||
{{template "host_type_copy" .Domain.Name}}
|
||
{{template "field_value" .SPFExample}}
|
||
|
||
<p class="muted">Merge into an existing SPF if the domain already has one —
|
||
do not publish a second record.</p>
|
||
</div>
|
||
|
||
<div class="card" id="dmarc">
|
||
<div class="card-head">
|
||
<h2>DMARC record</h2>
|
||
{{template "card_help" (dict "ID" "dmarc" "Title" "What this record does")}}
|
||
</div>
|
||
|
||
{{template "host_type_copy" .DMARCName}}
|
||
|
||
<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.{{if not .DMARCIngestEnabled}}
|
||
SelfPost does not receive inbound mail — use a mailbox on another domain.{{end}}</p>
|
||
{{end}}
|
||
{{if and .DMARCIngestEnabled .ResolvedDMARCEmail}}
|
||
<p class="muted"><a href="/dmarc/domains/{{.Domain.ID}}">View DMARC reports</a> for this domain.</p>
|
||
{{end}}
|
||
|
||
{{if .NeedsReportAuth}}
|
||
<p class="check-col-title">Report authorization</p>
|
||
{{template "host_type_copy" .ReportAuthName}}
|
||
{{template "field_value" .ReportAuthValue}}
|
||
{{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.
|
||
Report address is set under <a href="#domain-settings">Domain settings</a>.</p>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="split">
|
||
<div class="card" id="connection">
|
||
<div class="card-head">
|
||
<h2>Connection settings</h2>
|
||
{{template "card_help" (dict "ID" "connection" "Title" "How to connect")}}
|
||
</div>
|
||
<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 connection 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 class="card" id="send-stats">
|
||
<h2>Sending statistics ({{.StatsWindowDays}} days)</h2>
|
||
<p class="muted">One message = one queue id (many recipients count once).
|
||
Level-1 refusals are not in the send log — totals under-count strict IP
|
||
limits.{{if .StatsRetentionWarning}} Send log retention is shorter than
|
||
30 days; statistics use the last {{.StatsWindowDays}} days only.{{end}}</p>
|
||
<div class="field-pair">
|
||
<div>
|
||
<label>Total messages</label>
|
||
<span>{{.DomainStats.Total}}</span>
|
||
</div>
|
||
<div>
|
||
<label>Peak rate (msg/h)</label>
|
||
<span>{{.DomainStats.PeakPerHour}}</span>
|
||
</div>
|
||
<div>
|
||
<label>Average rate (msg/h)</label>
|
||
<span>{{.DomainStats.AvgPerHour}}</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="card" id="applications">
|
||
<div class="card-head">
|
||
<h2>Applications</h2>
|
||
{{template "card_help" (dict "ID" "apps" "Title" "What an application is")}}
|
||
</div>
|
||
<p class="muted">SASL logins for this domain — wildcard (*@domain) or a fixed
|
||
address list.</p>
|
||
|
||
{{if .Apps}}
|
||
<ul class="apps">
|
||
{{range .Apps}}
|
||
<li class="app">
|
||
<p class="app-login">{{.Login}}</p>
|
||
<p class="app-addr muted">
|
||
{{if eq .AddressMode $.Wildcard}}Any address of the domain — *@{{$.Domain.Name}}
|
||
{{else}}Fixed list — {{range $i, $a := .Addresses}}{{if $i}}, {{end}}{{$a}}{{end}}{{end}}
|
||
</p>
|
||
<p class="muted app-stats">{{.Stats.Total}} msg / {{$.StatsWindowDays}}d ·
|
||
peak {{.Stats.PeakPerHour}} msg/h · avg {{.Stats.AvgPerHour}} msg/h</p>
|
||
<!-- One Edit panel (mode ‖ rate limit) opened by a checkbox and label
|
||
rather than <details>, so the button row stays intact — see
|
||
.panel-toggle in panel.css. -->
|
||
<div class="actions">
|
||
<input class="panel-toggle t-edit" id="edit-{{.ID}}" type="checkbox">
|
||
<label class="toggle for-edit" for="edit-{{.ID}}">Edit{{if .HasLimit}} <span class="st st-ok">limit</span>{{end}}</label>
|
||
<form class="inline" method="post" action="/applications/{{.ID}}/password"
|
||
data-confirm="Regenerate the password for {{.Login}}? The current password stops working immediately.">
|
||
<button type="submit">New password</button>
|
||
</form>
|
||
<form class="inline" method="post" action="/applications/{{.ID}}/delete"
|
||
data-confirm="Delete application {{.Login}}? Its credentials stop working immediately.">
|
||
<button type="submit" class="danger">Delete</button>
|
||
</form>
|
||
<div class="panel panel-edit">
|
||
<div class="check-cols">
|
||
<div class="check-col">
|
||
<p class="check-col-title">Address mode</p>
|
||
<p class="muted">Which From addresses this application may use.</p>
|
||
<form method="post" action="/applications/{{.ID}}/mode">
|
||
<select name="mode" data-list-mode="{{$.List}}" aria-label="Address mode">
|
||
<option value="{{$.Wildcard}}" {{if eq .AddressMode $.Wildcard}}selected{{end}}>Any address of the domain</option>
|
||
<option value="{{$.List}}" {{if eq .AddressMode $.List}}selected{{end}}>Specific addresses (list)</option>
|
||
</select>
|
||
<div data-addresses>
|
||
<label>Addresses (one per line or comma-separated)</label>
|
||
<textarea name="addresses" rows="3" placeholder="alerts@{{$.Domain.Name}}">{{range $i, $a := .Addresses}}{{if $i}}
|
||
{{end}}{{$a}}{{end}}</textarea>
|
||
</div>
|
||
<button type="submit">Save mode</button>
|
||
</form>
|
||
</div>
|
||
<div class="check-col">
|
||
<p class="check-col-title">Client IP allow-list
|
||
{{if .AuthIPRestrict}}<span class="st st-ok">active</span>{{else}}<span class="st st-unknown">off</span>{{end}}</p>
|
||
<p class="muted">When enabled, only these addresses may submit mail as
|
||
this application; a login from anywhere else is refused with a 4xx at
|
||
<code>MAIL FROM</code>. When off, any client IP is allowed. Like the
|
||
level-2 rate limit this runs in the journal-milter and is fail-open —
|
||
it is a sending control, not an authentication boundary.</p>
|
||
<form method="post" action="/applications/{{.ID}}/authips">
|
||
<label class="check-row">
|
||
<input type="checkbox" name="auth_ip_restrict" value="1"
|
||
{{if .AuthIPRestrict}}checked{{end}}>
|
||
Restrict to listed IPs
|
||
</label>
|
||
<textarea name="auth_allowed_ips" rows="2" placeholder="203.0.113.10"
|
||
aria-label="Allowed client IPs">{{.AuthIPsText}}</textarea>
|
||
<button type="submit">Save IP restriction</button>
|
||
</form>
|
||
</div>
|
||
<div class="check-col">
|
||
<p class="check-col-title">Level-2 rate limit
|
||
{{if .HasLimit}}<span class="st st-ok">active</span>{{else}}<span class="st st-unknown">inactive</span>{{end}}</p>
|
||
<p class="muted">Overrides the domain limit for this application — the
|
||
ceiling may be <em>higher or lower</em> than the domain setting
|
||
(≤ level 1). When unset, the domain limit{{if $.DomainHasRL}}
|
||
({{$.DomainRLMaxNum}}){{end}} or level 1 applies.</p>
|
||
<form id="rl-{{.ID}}" method="post" action="/applications/{{.ID}}/ratelimit">
|
||
<label for="mode-{{.ID}}">Limit mode</label>
|
||
<select id="mode-{{.ID}}" name="mode" data-ratelimit-mode>
|
||
<option value="manual" {{if not .IsAuto}}selected{{end}}>Manual</option>
|
||
<option value="auto" {{if .IsAuto}}selected{{end}}>Auto (from statistics)</option>
|
||
</select>
|
||
<div data-manual-fields>
|
||
<div class="field-pair">
|
||
<div>
|
||
<label>Message limit (max {{$.L1Messages}})</label>
|
||
<input name="max_messages" type="number" min="1" max="{{$.L1Messages}}"
|
||
value="{{.MaxText}}" placeholder="{{$.L1Messages}}">
|
||
</div>
|
||
<div>
|
||
<label>Window (seconds)</label>
|
||
<input name="window_seconds" type="number" min="1" value="{{.WindowVal}}">
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<div data-auto-fields>
|
||
<label for="mult-{{.ID}}">Multiplier ({{$.MinAutoMultiplier}}–{{$.MaxAutoMultiplier}}, default {{$.DefaultAutoMultiplier}})</label>
|
||
<input id="mult-{{.ID}}" name="auto_multiplier" type="number" min="{{$.MinAutoMultiplier}}"
|
||
max="{{$.MaxAutoMultiplier}}" step="0.1" value="{{.AutoMultiplier}}">
|
||
<p class="muted">Computed limit: <strong>{{.MaxText}}</strong> messages /
|
||
{{$.L1Window}} s (level-1 window). Zero traffic keeps auto inactive until
|
||
messages are sent.{{if .AutoUpdated}} Last recalculated {{.AutoUpdated}}.{{end}}
|
||
Peak {{.Stats.PeakPerHour}} msg/h shown for comparison.</p>
|
||
</div>
|
||
</form>
|
||
<div class="panel-buttons">
|
||
<button type="submit" form="rl-{{.ID}}">Save limit</button>
|
||
{{if .IsAuto}}
|
||
<form class="inline" method="post" action="/applications/{{.ID}}/ratelimit/recalc">
|
||
<button type="submit">Recalculate now</button>
|
||
</form>
|
||
{{end}}
|
||
{{if .HasLimit}}
|
||
<form class="inline" method="post" action="/applications/{{.ID}}/ratelimit"
|
||
data-confirm="Remove the rate limit for {{.Login}}? The domain limit (or level 1) will apply.">
|
||
<input type="hidden" name="clear" value="1">
|
||
<button type="submit" class="danger">Remove limit</button>
|
||
</form>
|
||
{{end}}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</li>
|
||
{{end}}
|
||
</ul>
|
||
{{else}}
|
||
<p class="muted">No applications yet. Add one above to get started.</p>
|
||
{{end}}
|
||
</div>
|
||
|
||
<div class="card" id="domain-settings">
|
||
<div class="card-head">
|
||
<h2>Domain settings</h2>
|
||
{{template "card_help" (dict "ID" "domain-settings" "Title" "Reports and rate limits")}}
|
||
</div>
|
||
|
||
<div class="check-cols check-cols-rows">
|
||
<div class="check-col">
|
||
<p class="check-col-title">DMARC reports</p>
|
||
<p class="muted">Default comes from <a href="/settings">Settings</a>;
|
||
override per domain here.</p>
|
||
<div class="check-col-fields">
|
||
<form id="dmarc-domain" 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" data-custom-mode="custom">
|
||
<option value="inherit"{{if eq .DMARCRuaMode "inherit"}} selected{{end}}>Same as Settings{{if .ProfileDMARCEmail}} ({{.ProfileDMARCEmail}}){{end}}</option>
|
||
{{if .DMARCIngestEnabled}}<option value="hosted"{{if eq .DMARCRuaMode "hosted"}} selected{{end}}>SelfPost hosted ({{.HostedDMARCEmail}})</option>{{end}}
|
||
<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>
|
||
|
||
<div data-custom-address>
|
||
<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">
|
||
</div>
|
||
</form>
|
||
</div>
|
||
<div class="check-col-actions">
|
||
<button type="submit" form="dmarc-domain">Save DMARC report settings</button>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="check-col">
|
||
<p class="check-col-title">Level-2 rate limit
|
||
{{if .DomainHasRL}}<span class="st st-ok">active</span>{{else}}<span class="st st-unknown">inactive</span>{{end}}</p>
|
||
<p class="muted">Level 1 backstop: {{.L1Messages}} messages / {{.L1Window}}s — <a href="/settings#rate-limits">Settings</a>.</p>
|
||
<div class="check-col-fields">
|
||
<form id="rl-domain" method="post" action="/domains/{{.Domain.ID}}/ratelimit">
|
||
<label for="d_mode">Limit mode</label>
|
||
<select id="d_mode" name="mode" data-ratelimit-mode>
|
||
<option value="manual" {{if not .DomainRLAuto}}selected{{end}}>Manual</option>
|
||
<option value="auto" {{if .DomainRLAuto}}selected{{end}}>Auto (from statistics)</option>
|
||
</select>
|
||
<div data-manual-fields>
|
||
<div class="field-pair">
|
||
<div>
|
||
<label for="d_max">Message limit (max {{.L1Messages}})</label>
|
||
<input id="d_max" name="max_messages" type="number" min="1" max="{{.L1Messages}}"
|
||
value="{{.DomainRLMax}}" placeholder="{{.L1Messages}}">
|
||
</div>
|
||
<div>
|
||
<label for="d_win">Window (seconds)</label>
|
||
<input id="d_win" name="window_seconds" type="number" min="1" value="{{.DomainRLWin}}">
|
||
</div>
|
||
</div>
|
||
<p class="muted">Applies to every client IP on this domain. Leave the
|
||
message limit empty to use level 1 only.</p>
|
||
</div>
|
||
<div data-auto-fields>
|
||
<label for="d_mult">Multiplier ({{.MinAutoMultiplier}}–{{.MaxAutoMultiplier}}, default {{.DefaultAutoMultiplier}})</label>
|
||
<input id="d_mult" name="auto_multiplier" type="number" min="{{.MinAutoMultiplier}}"
|
||
max="{{.MaxAutoMultiplier}}" step="0.1" value="{{.DomainRLMultiplier}}">
|
||
<p class="muted">Computed limit: <strong>{{.DomainRLMax}}</strong> messages /
|
||
{{.L1Window}} s (level-1 window). Zero traffic keeps auto inactive until
|
||
messages are sent.{{if .DomainRLAutoUpdated}} Last recalculated {{.DomainRLAutoUpdated}}.{{end}}
|
||
Peak {{.DomainStats.PeakPerHour}} msg/h shown for comparison.</p>
|
||
</div>
|
||
</form>
|
||
</div>
|
||
<div class="check-col-actions">
|
||
<button type="submit" form="rl-domain">Save limit</button>
|
||
{{if .DomainRLAuto}}
|
||
<form class="inline" method="post" action="/domains/{{.Domain.ID}}/ratelimit/recalc">
|
||
<button type="submit">Recalculate now</button>
|
||
</form>
|
||
{{end}}
|
||
{{if .DomainHasRL}}
|
||
<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.">
|
||
<input type="hidden" name="clear" value="1">
|
||
<button type="submit" class="danger">Remove limit</button>
|
||
</form>
|
||
{{end}}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="split">
|
||
<div class="card" id="export">
|
||
<div class="card-head">
|
||
<h2>Export domain</h2>
|
||
{{template "card_help" (dict "ID" "export" "Title" "What the file contains")}}
|
||
</div>
|
||
<p class="muted"><strong>Secret file</strong> — transfer securely, or encrypt
|
||
below as <code>.spde</code>.</p>
|
||
{{if .ExportErr}}<p class="error">{{.ExportErr}}</p>{{end}}
|
||
<form method="post" action="/domains/{{.Domain.ID}}/export">
|
||
{{template "encryptfields" .}}
|
||
<button type="submit">Export domain</button>
|
||
</form>
|
||
</div>
|
||
|
||
<div class="card" id="danger">
|
||
<h2>Danger zone</h2>
|
||
<p class="muted">Deletes the DKIM key and every application on this domain.</p>
|
||
<a class="danger" href="/domains/{{.Domain.ID}}/delete">Delete domain</a>
|
||
</div>
|
||
</div>
|
||
{{end}}
|