Files
mailgosend/web/admin/templates/domain.html
T

411 lines
18 KiB
HTML

{{define "title"}}Domain — {{.Domain.Name}}{{end}}
{{define "content"}}
<div class="flex items-center gap-3 mb-6">
<a href="/admin/domains" class="text-gray-400 text-sm hover:text-white">Domains</a>
<span class="text-gray-600">/</span>
<h1 class="text-xl font-bold text-white">{{.Domain.Name}}</h1>
{{if .Domain.Enabled}}<span class="badge badge-green">enabled</span>{{else}}<span class="badge badge-red">disabled</span>{{end}}
</div>
<div style="display:grid;grid-template-columns:1fr 1fr;gap:1.5rem">
<!-- Left column -->
<div>
<!-- Toggle enable -->
<div class="card">
<div class="text-sm font-semibold text-gray-300 mb-3">Enable / Disable</div>
<form method="POST" action="/admin/domains/{{.Domain.ID}}/enable">
<input type="hidden" name="_csrf" value="{{.CSRF}}">
{{if .Domain.Enabled}}
<input type="hidden" name="enabled" value="0">
<button type="submit" class="btn btn-danger">Disable domain</button>
{{else}}
<input type="hidden" name="enabled" value="1">
<button type="submit" class="btn btn-primary">Enable domain</button>
{{end}}
</form>
</div>
<!-- Limits -->
<div class="card">
<div class="text-sm font-semibold text-gray-300 mb-3">Limits</div>
<form method="POST" action="/admin/domains/{{.Domain.ID}}/limits">
<input type="hidden" name="_csrf" value="{{.CSRF}}">
<div class="field">
<label>Max users (0 = unlimited)</label>
<input type="number" name="max_users" value="{{.Domain.MaxUsers}}" min="0" max="100000">
</div>
<div class="field">
<label>Max quota per user (MB, 0 = unlimited)</label>
<input type="number" name="max_quota_mb" value="{{mb .Domain.MaxQuotaBytes}}" min="0" max="1048576">
</div>
<button type="submit" class="btn btn-primary btn-sm">Save limits</button>
</form>
</div>
<!-- Delete -->
<div class="card" style="border:1px solid #7f1d1d">
<div class="text-sm font-semibold text-red-400 mb-2">Delete domain</div>
<div class="text-xs text-gray-400 mb-3">Permanently deletes the domain and ALL users and messages within it. This cannot be undone.</div>
<form method="POST" action="/admin/domains/{{.Domain.ID}}/delete"
onsubmit="return confirm('Delete domain {{.Domain.Name}} and all its data?')">
<input type="hidden" name="_csrf" value="{{.CSRF}}">
<button type="submit" class="btn btn-danger btn-sm">Delete domain</button>
</form>
</div>
</div>
<!-- Right column -->
<div>
<!-- DKIM key management -->
<div class="card">
<div class="text-sm font-semibold text-gray-300 mb-3">DKIM key</div>
{{if .Domain.DKIMPublic}}
<div class="text-xs text-gray-400 mb-2">
Selector: <span class="text-white">{{.Domain.DKIMSelector}}</span> /
Algorithm: <span class="text-white">{{.Domain.DKIMAlgo}}</span>
</div>
{{else}}
<div class="text-xs text-yellow-400 mb-3">No DKIM key generated yet.</div>
{{end}}
<form method="POST" action="/admin/domains/{{.Domain.ID}}/dkim">
<input type="hidden" name="_csrf" value="{{.CSRF}}">
<div class="field">
<label>Algorithm for new key</label>
<select name="algo">
<option value="rsa2048" {{if eq .Domain.DKIMAlgo "rsa2048"}}selected{{end}}>RSA-2048</option>
<option value="ed25519" {{if eq .Domain.DKIMAlgo "ed25519"}}selected{{end}}>Ed25519</option>
</select>
</div>
<button type="submit" class="btn btn-primary btn-sm"
onclick="return !{{if .Domain.DKIMPublic}}confirm('Regenerate DKIM key? Old signatures become invalid.'){{else}}false{{end}}">
{{if .Domain.DKIMPublic}}Regenerate DKIM key{{else}}Generate DKIM key{{end}}
</button>
</form>
</div>
<!-- DMARC Monitoring -->
<div class="card">
<div class="text-sm font-semibold text-gray-300 mb-3">DMARC Monitoring</div>
{{if .Domain.DMARCRua}}
<div class="text-xs text-gray-400 mb-1">Monitoring address</div>
<div style="font-family:monospace;font-size:.75rem;color:#60a5fa;word-break:break-all;margin-bottom:.75rem">{{.Domain.DMARCRua}}</div>
<div class="text-xs text-gray-500 mb-3">
Add this address to your DMARC DNS record as <span style="font-family:monospace;color:#9ca3af">rua=mailto:{{.Domain.DMARCRua}}</span>.
External mail servers will send aggregate reports here every 24 hours.
</div>
<div class="flex items-center gap-2">
<a href="/admin/domains/{{.Domain.ID}}/dmarc" class="btn btn-primary btn-sm">
View reports{{if .DMARCReportCount}} ({{.DMARCReportCount}}){{end}}
</a>
<form method="POST" action="/admin/domains/{{.Domain.ID}}/dmarc/disable"
onsubmit="return confirm('Disable DMARC monitoring? Incoming reports will no longer be processed.')">
<input type="hidden" name="_csrf" value="{{.CSRF}}">
<button type="submit" class="btn btn-danger btn-sm">Disable</button>
</form>
</div>
{{else}}
<div class="text-xs text-gray-400 mb-3">
Enable monitoring to receive DMARC aggregate reports from external mail servers.
A unique email address will be generated for this domain and reports delivered to it will be parsed and stored.
</div>
<form method="POST" action="/admin/domains/{{.Domain.ID}}/dmarc/enable">
<input type="hidden" name="_csrf" value="{{.CSRF}}">
<button type="submit" class="btn btn-primary btn-sm">Enable DMARC monitoring</button>
</form>
{{end}}
</div>
<!-- DNS Records -->
<div class="card">
<div class="flex items-center justify-between mb-3">
<div class="text-sm font-semibold text-gray-300">DNS Records</div>
<button onclick="checkDNS({{.Domain.ID}}, this)" class="btn btn-primary btn-sm">Check DNS</button>
</div>
<div class="text-xs font-semibold text-gray-400 mb-2" style="text-transform:uppercase;letter-spacing:.05em">Required</div>
<div class="text-xs text-gray-400 mb-1">MX</div>
<div class="dns-record" style="margin-bottom:.75rem">{{.MXRecord}}</div>
<div class="text-xs text-gray-400 mb-1">SPF</div>
<div class="dns-record" style="margin-bottom:.75rem">{{.SPFHint}}</div>
{{if .DKIMRecord}}
<div class="text-xs text-gray-400 mb-1">DKIM (selector: {{.Domain.DKIMSelector}})</div>
<div class="dns-record" style="margin-bottom:.75rem">{{.DKIMRecord}}</div>
{{else}}
<div class="text-xs text-yellow-400 mb-3">Generate a DKIM key above to get the DKIM DNS record.</div>
{{end}}
<div class="text-xs text-gray-400 mb-1">DMARC</div>
<div class="dns-record" style="margin-bottom:.625rem">{{.DMARCHint}}</div>
{{if .Domain.DMARCRua}}
<div class="text-xs text-gray-500 mb-1">Monitoring address: <span style="color:#60a5fa;font-family:monospace">{{.Domain.DMARCRua}}</span></div>
{{end}}
<div style="margin-bottom:1rem"></div>
<div class="text-xs font-semibold text-gray-400 mb-2" style="text-transform:uppercase;letter-spacing:.05em">Optional (autoconfiguration)</div>
<div class="text-xs text-gray-500 mb-2">Allows mail clients (Thunderbird, Outlook) to auto-discover server settings.</div>
<div class="text-xs text-gray-400 mb-1">Thunderbird autoconfig (CNAME)</div>
<div class="dns-record" style="margin-bottom:.75rem">{{.AutoconfigCNAME}}</div>
<div class="text-xs text-gray-400 mb-1">Outlook autodiscover (CNAME)</div>
<div class="dns-record" style="margin-bottom:.75rem">{{.AutodiscoverCNAME}}</div>
<div class="text-xs text-gray-400 mb-1">SMTP submission (SRV, RFC 6186)</div>
<div class="dns-record" style="margin-bottom:.75rem">{{.SMTPSRVRecord}}</div>
<div class="text-xs text-gray-400 mb-1">IMAP SSL (SRV, RFC 6186)</div>
<div class="dns-record">{{.IMAPSRVRecord}}</div>
<!-- DNS check results injected here -->
<div id="dns-check-{{.Domain.ID}}" style="display:none;margin-top:1.25rem;padding-top:1rem;border-top:1px solid #374151"></div>
</div>
<!-- IP Relay Rules -->
<div class="card">
<div class="text-sm font-semibold text-gray-300 mb-2">IP Relay Rules</div>
<div class="text-xs text-gray-400 mb-3">
Allow specific IP addresses (or CIDR ranges) to submit email for this domain
without SMTP authentication. Optionally restrict to specific sender addresses.
</div>
{{if .RelayIPRules}}
<div style="margin-bottom:.75rem;overflow:auto">
<table style="font-size:.7rem;width:100%;border-collapse:collapse">
<thead>
<tr style="color:#6b7280;text-align:left;border-bottom:1px solid #374151">
<th style="padding:.25rem .5rem">IP / CIDR</th>
<th style="padding:.25rem .5rem">Sender pattern</th>
<th style="padding:.25rem .5rem">Description</th>
<th style="padding:.25rem .5rem"></th>
</tr>
</thead>
<tbody>
{{range .RelayIPRules}}
<tr style="border-bottom:1px solid #1f2937">
<td style="padding:.25rem .5rem;font-family:monospace;color:#93c5fd">{{.CIDR}}</td>
<td style="padding:.25rem .5rem;font-family:monospace;color:#a78bfa">{{.SenderPattern}}</td>
<td style="padding:.25rem .5rem;color:#9ca3af">{{.Description}}</td>
<td style="padding:.25rem .5rem;text-align:right">
<form method="POST" action="/admin/domains/{{$.Domain.ID}}/iprelay/{{.ID}}/delete" style="margin:0"
onsubmit="return confirm('Remove this IP relay rule?')">
<input type="hidden" name="_csrf" value="{{$.CSRF}}">
<button type="submit" class="btn btn-danger btn-sm" style="padding:.125rem .5rem;font-size:.7rem">Remove</button>
</form>
</td>
</tr>
{{end}}
</tbody>
</table>
</div>
{{else}}
<div class="text-xs text-gray-500 mb-3">No IP relay rules configured.</div>
{{end}}
<form method="POST" action="/admin/domains/{{.Domain.ID}}/iprelay/add">
<input type="hidden" name="_csrf" value="{{.CSRF}}">
<div style="display:grid;grid-template-columns:1fr 1fr;gap:.5rem">
<div class="field" style="margin:0">
<label style="font-size:.7rem">IP or CIDR</label>
<input type="text" name="cidr" required placeholder="10.0.0.1 or 10.0.0.0/24" maxlength="50">
</div>
<div class="field" style="margin:0">
<label style="font-size:.7rem">Sender pattern</label>
<input type="text" name="sender_pattern" required placeholder="*@{{.Domain.Name}}" maxlength="255">
</div>
</div>
<div class="field" style="margin:.5rem 0 .75rem">
<label style="font-size:.7rem">Description (optional)</label>
<input type="text" name="description" placeholder="Internal mail server" maxlength="255">
</div>
<button type="submit" class="btn btn-primary btn-sm">Add rule</button>
</form>
</div>
</div>
</div>
<!-- Users -->
<div class="mt-4">
<div class="flex items-center justify-between mb-2">
<h2 class="text-sm font-semibold text-gray-300">Users ({{len .Users}})</h2>
<a href="/admin/users" class="btn btn-primary btn-sm">Add user</a>
</div>
<div class="card" style="padding:0;overflow:hidden">
{{if .Users}}
<table>
<thead>
<tr>
<th>Email</th>
<th>Display name</th>
<th>Status</th>
<th>Role</th>
<th>Quota used</th>
<th>Last login</th>
<th></th>
</tr>
</thead>
<tbody>
{{range .Users}}
<tr>
<td><a href="/admin/users/{{.ID}}" class="text-blue-400 hover:underline">{{.Email}}</a></td>
<td class="text-gray-300">{{.DisplayName}}</td>
<td>
{{if .Enabled}}<span class="badge badge-green">active</span>
{{else}}<span class="badge badge-red">disabled</span>{{end}}
</td>
<td>
{{if .Admin}}<span class="badge badge-yellow">admin</span>
{{else if .DomainAdmin}}<span class="badge badge-gray">domain admin</span>
{{else}}<span class="badge badge-gray">user</span>{{end}}
</td>
<td class="text-gray-400 text-xs">{{humanBytes .UsedBytes}} / {{if .QuotaBytes}}{{humanBytes .QuotaBytes}}{{else}}unlimited{{end}}</td>
<td class="text-gray-400 text-xs">{{if isZero .LastLogin}}never{{else}}{{shortTime .LastLogin}}{{end}}</td>
<td><a href="/admin/users/{{.ID}}" class="btn btn-primary btn-sm">Edit</a></td>
</tr>
{{end}}
</tbody>
</table>
{{else}}
<div class="p-6 text-center text-gray-500 text-sm">No users in this domain.</div>
{{end}}
</div>
</div>
<!-- Relay Accounts -->
<div class="mt-4">
<div class="flex items-center justify-between mb-2">
<h2 class="text-sm font-semibold text-gray-300">Relay Accounts ({{len .RelayAccounts}})</h2>
</div>
<div class="card" style="padding:0;overflow:hidden">
{{if .RelayAccounts}}
<table>
<thead>
<tr>
<th>Username</th>
<th>Display name</th>
<th>Status</th>
<th>Description</th>
<th>Created</th>
<th></th>
</tr>
</thead>
<tbody>
{{range .RelayAccounts}}
<tr>
<td style="font-family:monospace;font-size:.8rem">
<a href="/admin/domains/{{$.Domain.ID}}/relayaccount/{{.ID}}" class="text-blue-400 hover:underline">{{.Username}}</a>
</td>
<td class="text-gray-300">{{.DisplayName}}</td>
<td>
{{if .Enabled}}<span class="badge badge-green">active</span>
{{else}}<span class="badge badge-red">disabled</span>{{end}}
</td>
<td class="text-gray-400 text-xs">{{.Description}}</td>
<td class="text-gray-400 text-xs">{{shortTime .CreatedAt}}</td>
<td><a href="/admin/domains/{{$.Domain.ID}}/relayaccount/{{.ID}}" class="btn btn-primary btn-sm">Manage</a></td>
</tr>
{{end}}
</tbody>
</table>
{{else}}
<div class="p-6 text-center text-gray-500 text-sm">No relay accounts configured.</div>
{{end}}
</div>
<!-- Create relay account form -->
<div class="card mt-3">
<div class="text-sm font-semibold text-gray-300 mb-3">Create relay account</div>
<form method="POST" action="/admin/domains/{{.Domain.ID}}/relayaccount">
<input type="hidden" name="_csrf" value="{{.CSRF}}">
<div style="display:grid;grid-template-columns:2fr 1fr 1fr;gap:.75rem;margin-bottom:.75rem">
<div class="field" style="margin:0">
<label>Username (email or arbitrary string, max 240 chars)</label>
<input type="text" name="username" required maxlength="240" placeholder="relay@example.com or myapp-relay">
</div>
<div class="field" style="margin:0">
<label>Display name</label>
<input type="text" name="display_name" maxlength="255" placeholder="My App">
</div>
<div class="field" style="margin:0">
<label>Password (min 8 chars)</label>
<input type="password" name="password" required minlength="8" maxlength="1024">
</div>
</div>
<div style="display:grid;grid-template-columns:2fr auto;gap:.75rem;align-items:flex-end">
<div class="field" style="margin:0">
<label>Description (optional)</label>
<input type="text" name="description" maxlength="255" placeholder="Internal application relay">
</div>
<button type="submit" class="btn btn-primary">Create</button>
</div>
</form>
</div>
</div>
<style>
.dns-record {
background: #111827;
border-radius: .375rem;
padding: .5rem .625rem;
font-size: .7rem;
color: #93c5fd;
word-break: break-all;
line-height: 1.6;
font-family: monospace;
}
</style>
<script>
function checkDNS(id, btn) {
var orig = btn.textContent;
btn.disabled = true;
btn.textContent = 'Checking...';
var div = document.getElementById('dns-check-' + id);
div.style.display = 'block';
div.innerHTML = '<div style="font-size:.75rem;color:#9ca3af">Performing DNS lookups...</div>';
fetch('/admin/domains/' + id + '/dnscheck')
.then(function(r) {
if (!r.ok) throw new Error('HTTP ' + r.status);
return r.json();
})
.then(function(data) {
var html = '<div style="font-size:.75rem;font-weight:600;color:#d1d5db;margin-bottom:.625rem">DNS Check Results</div>';
html += dnsRow('MX', data.mx);
html += dnsRow('SPF', data.spf);
html += dnsRow('DKIM', data.dkim);
html += dnsRow('DMARC', data.dmarc);
div.innerHTML = html;
})
.catch(function(e) {
div.innerHTML = '<div style="font-size:.75rem;color:#f87171">DNS check failed: ' + esc(e.message) + '</div>';
})
.finally(function() {
btn.disabled = false;
btn.textContent = orig;
});
}
function dnsRow(name, rec) {
var colors = {ok:'#6ee7b7', warn:'#fbbf24', error:'#f87171', missing:'#9ca3af'};
var labels = {ok:'OK', warn:'WARN', error:'FAIL', missing:'MISSING'};
var c = colors[rec.status] || '#9ca3af';
var l = labels[rec.status] || rec.status.toUpperCase();
var h = '<div style="margin-bottom:.5rem;font-size:.75rem">';
h += '<span style="color:' + c + ';font-weight:700;font-family:monospace">[' + l + ']</span> ';
h += '<span style="color:#d1d5db;font-weight:600">' + esc(name) + '</span> ';
h += '<span style="color:#9ca3af">' + esc(rec.message) + '</span>';
if (rec.found) {
h += '<div style="color:#6b7280;margin-top:.25rem;word-break:break-all;padding-left:1rem;font-size:.7rem;font-family:monospace">' + esc(rec.found) + '</div>';
}
h += '</div>';
return h;
}
function esc(s) {
return (s||'').replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;').replace(/"/g,'&quot;');
}
</script>
{{end}}