291 lines
18 KiB
HTML
291 lines
18 KiB
HTML
{{define "title"}}DKIM Keys - Email Server{{end}}
|
|
|
|
{{define "extra_css"}}
|
|
<style>
|
|
.dns-record { font-family: 'Courier New', monospace; color: black; background-color: var(--bs-gray-100); border-radius: 0.375rem; padding: 0.75rem; border: 1px solid var(--bs-border-color); word-break: break-all; }
|
|
.status-indicator { width: 12px; height: 12px; border-radius: 50%; display: inline-block; margin-right: 0.5rem; }
|
|
.status-success { background-color: #28a745; }
|
|
.status-warning { background-color: #ffc107; }
|
|
.status-danger { background-color: #dc3545; }
|
|
</style>
|
|
{{end}}
|
|
|
|
{{define "content"}}
|
|
<div class="container-fluid">
|
|
<div class="d-flex justify-content-between align-items-center mb-4">
|
|
<h2><i class="bi bi-shield-check me-2"></i>DKIM Key Management</h2>
|
|
<div class="btn-group">
|
|
<button class="btn btn-outline-primary me-2" data-bs-toggle="modal" data-bs-target="#createDKIMModal"><i class="bi bi-plus-circle me-2"></i>Create DKIM</button>
|
|
<button class="btn btn-outline-info" data-action="check-all-dns"><i class="bi bi-arrow-clockwise me-2"></i>Check All DNS</button>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="modal fade" id="createDKIMModal" tabindex="-1" aria-hidden="true">
|
|
<div class="modal-dialog">
|
|
<div class="modal-content">
|
|
<form id="createDKIMForm" method="post" action="/pymta-manager/dkim/create">
|
|
<div class="modal-header">
|
|
<h5 class="modal-title">Create New DKIM Key</h5>
|
|
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
|
</div>
|
|
<div class="modal-body">
|
|
<div class="mb-3">
|
|
<label for="dkimDomain" class="form-label">Domain</label>
|
|
<select class="form-select" id="dkimDomain" name="domain" required>
|
|
<option value="" disabled selected>Select domain</option>
|
|
{{range .dkim_data}}<option value="{{.domain.domain_name}}">{{.domain.domain_name}}</option>{{end}}
|
|
</select>
|
|
</div>
|
|
<div class="mb-3">
|
|
<label for="dkimSelector" class="form-label">Selector (optional)</label>
|
|
<input type="text" class="form-control" id="dkimSelector" name="selector" maxlength="32" placeholder="Leave blank for random selector">
|
|
</div>
|
|
</div>
|
|
<div class="modal-footer">
|
|
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
|
|
<button type="submit" class="btn btn-primary">Create</button>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{{range .dkim_data}}
|
|
{{$domain := .domain}}{{$key := .dkim_key}}{{$slug := dotToDash $domain.domain_name}}
|
|
<div class="card mb-4" id="domain-{{$slug}}" data-is-active="{{$key.IsActive}}">
|
|
<div class="card-header">
|
|
<div class="d-flex justify-content-between align-items-center">
|
|
<div class="flex-grow-1 card-header-clickable" style="cursor: pointer;" data-bs-toggle="collapse" data-bs-target="#collapse-{{$slug}}">
|
|
<h5 class="mb-0">
|
|
<i class="bi bi-server me-2"></i>
|
|
{{$domain.domain_name}}
|
|
{{if $key.IsActive}}<span class="badge bg-success ms-2">Active</span>{{else}}<span class="badge bg-secondary ms-2">Inactive</span>{{end}}
|
|
</h5>
|
|
</div>
|
|
<div class="btn-group btn-group-sm me-2">
|
|
<button class="btn btn-outline-primary" data-action="check-dns" data-domain="{{$domain.domain_name}}" data-selector="{{$key.Selector}}" onclick="event.stopPropagation();">
|
|
<i class="bi bi-search me-1"></i>Check DNS
|
|
</button>
|
|
<div class="btn-group btn-group-sm" role="group">
|
|
<a href="/pymta-manager/dkim/{{$key.ID}}/edit" class="btn btn-outline-info" onclick="event.stopPropagation();"><i class="bi bi-pencil me-1"></i>Edit</a>
|
|
<form method="post" action="/pymta-manager/dkim/{{$key.ID}}/toggle" class="d-inline">
|
|
{{if $key.IsActive}}
|
|
<button type="submit" class="btn btn-outline-warning" onclick="event.stopPropagation();" title="Disable DKIM"><i class="bi bi-pause-circle me-1"></i>Disable</button>
|
|
{{else}}
|
|
<button type="submit" class="btn btn-outline-success" onclick="event.stopPropagation();" title="Enable DKIM"><i class="bi bi-play-circle me-1"></i>Enable</button>
|
|
{{end}}
|
|
</form>
|
|
<form method="post" action="/pymta-manager/dkim/{{$key.ID}}/remove" class="d-inline">
|
|
<button type="submit" class="btn btn-outline-danger" onclick="event.stopPropagation();" data-confirm="Permanently remove the DKIM key for {{$domain.domain_name}}? You will lose the ability to sign emails until you regenerate a new key.">
|
|
<i class="bi bi-trash me-1"></i>Remove
|
|
</button>
|
|
</form>
|
|
</div>
|
|
<form method="post" action="/pymta-manager/dkim/{{$domain.id}}/regenerate" class="d-inline" onsubmit="event.stopPropagation();">
|
|
<button type="submit" class="btn btn-outline-warning" onclick="event.stopPropagation();"><i class="bi bi-arrow-clockwise me-1"></i>Regenerate</button>
|
|
</form>
|
|
</div>
|
|
<div class="card-header-clickable" style="cursor: pointer;" data-bs-toggle="collapse" data-bs-target="#collapse-{{$slug}}">
|
|
<i class="bi bi-chevron-down" id="chevron-{{$slug}}"></i>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="collapse" id="collapse-{{$slug}}">
|
|
<div class="card-body">
|
|
<div class="row">
|
|
<div class="col-lg-6 mb-3">
|
|
<h6>
|
|
<i class="bi bi-key me-2"></i>DKIM DNS Record
|
|
<span class="dns-status" id="dkim-status-{{$slug}}"><span class="status-indicator status-warning"></span><small class="text-muted">Active (DNS not checked)</small></span>
|
|
</h6>
|
|
<div class="mb-2"><strong>Name:</strong><div class="dns-record">{{.dns_record.name}}</div></div>
|
|
<div class="mb-2"><strong>Type:</strong> TXT</div>
|
|
<div class="mb-2"><strong>Value:</strong><div class="dns-record">{{.dns_record.value}}</div></div>
|
|
<button class="btn btn-outline-secondary btn-sm" onclick="copyToClipboard('{{.dns_record.value}}')"><i class="bi bi-clipboard me-1"></i>Copy Value</button>
|
|
</div>
|
|
<div class="col-lg-6 mb-3">
|
|
<h6>
|
|
<i class="bi bi-shield-lock me-2"></i>SPF DNS Record
|
|
<span class="dns-status" id="spf-status-{{$slug}}"><span class="status-indicator status-warning"></span><small class="text-muted">Not checked</small></span>
|
|
</h6>
|
|
<div class="mb-2"><strong>Name:</strong><div class="dns-record">{{$domain.domain_name}}</div></div>
|
|
<div class="mb-2"><strong>Type:</strong> TXT</div>
|
|
{{if .existing_spf}}<div class="mb-2"><strong>Current SPF:</strong><div class="dns-record">{{.existing_spf}}</div></div>{{end}}
|
|
<div class="mb-2"><strong>Recommended SPF:</strong><div class="dns-record">{{.recommended_spf}}</div></div>
|
|
<button class="btn btn-outline-secondary btn-sm" onclick="copyToClipboard('{{.recommended_spf}}')"><i class="bi bi-clipboard me-1"></i>Copy SPF</button>
|
|
</div>
|
|
</div>
|
|
<div class="row">
|
|
<div class="col-12">
|
|
<h6><i class="bi bi-info-circle me-2"></i>Key Information</h6>
|
|
<div class="row">
|
|
<div class="col-md-3"><strong>Selector:</strong><br><code>{{$key.Selector}}</code></div>
|
|
<div class="col-md-3"><strong>Created:</strong><br>{{strftime "%Y-%m-%d %H:%M" $key.CreatedAt}}</div>
|
|
<div class="col-md-3"><strong>Server IP:</strong><br><code>{{.public_ip}}</code></div>
|
|
<div class="col-md-3"><strong>Status:</strong><br>{{if $key.IsActive}}<span class="text-success">Active</span>{{else}}<span class="text-secondary">Inactive</span>{{end}}</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
{{end}}
|
|
|
|
{{if .old_dkim_data}}
|
|
<div class="card mb-4">
|
|
<div class="card-header"><h4 class="mb-0"><i class="bi bi-archive me-2"></i>Old DKIM Keys <span class="badge bg-secondary ms-2">{{len .old_dkim_data}}</span></h4></div>
|
|
<div class="card-body">
|
|
<p class="text-muted mb-3">These keys have been replaced or disabled. They are kept for reference and can be permanently removed.</p>
|
|
{{range .old_dkim_data}}
|
|
{{$domain := .domain}}{{$key := .dkim_key}}
|
|
<div class="card mb-3 border-secondary">
|
|
<div class="card-header bg-dark">
|
|
<div class="d-flex justify-content-between align-items-center">
|
|
<div>
|
|
<h6 class="mb-0"><i class="bi bi-server me-2"></i>{{$domain.domain_name}}<span class="badge bg-secondary ms-2">{{.status_text}}</span></h6>
|
|
<small class="text-muted">Selector: <code>{{$key.Selector}}</code> | Created: {{strftime "%Y-%m-%d %H:%M" $key.CreatedAt}}</small>
|
|
</div>
|
|
<div class="btn-group btn-group-sm">
|
|
<form method="post" action="/pymta-manager/dkim/{{$key.ID}}/toggle" class="d-inline">
|
|
<button type="submit" class="btn btn-outline-success btn-sm"><i class="bi bi-play-circle me-1"></i>Reactivate</button>
|
|
</form>
|
|
<form method="post" action="/pymta-manager/dkim/{{$key.ID}}/remove" class="d-inline">
|
|
<button type="submit" class="btn btn-outline-danger btn-sm" data-confirm="Permanently remove this old DKIM key? This action cannot be undone."><i class="bi bi-trash me-1"></i>Remove</button>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
{{end}}
|
|
</div>
|
|
</div>
|
|
{{end}}
|
|
|
|
{{if not .dkim_data}}
|
|
<div class="card">
|
|
<div class="card-body text-center py-5">
|
|
<i class="bi bi-shield-x text-muted" style="font-size: 4rem;"></i>
|
|
<h4 class="text-muted mt-3">No DKIM Keys Found</h4>
|
|
<p class="text-muted">Add domains first to automatically generate DKIM keys</p>
|
|
<a href="/pymta-manager/domains/add" class="btn btn-primary"><i class="bi bi-plus-circle me-2"></i>Add Domain</a>
|
|
</div>
|
|
</div>
|
|
{{end}}
|
|
</div>
|
|
|
|
<div class="modal fade" id="dnsResultModal" tabindex="-1">
|
|
<div class="modal-dialog modal-lg">
|
|
<div class="modal-content">
|
|
<div class="modal-header"><h5 class="modal-title">DNS Check Results</h5><button type="button" class="btn-close" data-bs-dismiss="modal"></button></div>
|
|
<div class="modal-body" id="dnsResults"></div>
|
|
<div class="modal-footer"><button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Close</button></div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
{{end}}
|
|
|
|
{{define "extra_js"}}
|
|
<script>
|
|
function copyToClipboard(text) {
|
|
navigator.clipboard.writeText(text).then(function() { showToast('Copied to clipboard!', 'success'); }, function(err) { showToast('Failed to copy: ' + err, 'danger'); });
|
|
}
|
|
|
|
async function checkDomainDNS(domain, selector) {
|
|
const dkimStatus = document.getElementById(`dkim-status-${domain.replace('.', '-')}`);
|
|
const spfStatus = document.getElementById(`spf-status-${domain.replace('.', '-')}`);
|
|
dkimStatus.innerHTML = '<span class="status-indicator status-warning"></span><small class="text-muted">Checking...</small>';
|
|
spfStatus.innerHTML = '<span class="status-indicator status-warning"></span><small class="text-muted">Checking...</small>';
|
|
try {
|
|
const dkimResponse = await fetch('/pymta-manager/dkim/check_dns', { method: 'POST', headers: {'Content-Type': 'application/x-www-form-urlencoded'}, body: new URLSearchParams({domain, selector}) });
|
|
const dkimResult = await dkimResponse.json();
|
|
const spfResponse = await fetch('/pymta-manager/dkim/check_spf', { method: 'POST', headers: {'Content-Type': 'application/x-www-form-urlencoded'}, body: new URLSearchParams({domain}) });
|
|
const spfResult = await spfResponse.json();
|
|
const domainCard = document.getElementById(`domain-${domain.replace('.', '-')}`);
|
|
const isActive = domainCard && domainCard.dataset.isActive === 'true';
|
|
if (isActive) {
|
|
dkimStatus.innerHTML = dkimResult.success
|
|
? '<span class="status-indicator status-success"></span><small class="text-success">Active & Configured</small>'
|
|
: '<span class="status-indicator" style="background-color: #fd7e14;"></span><small class="text-warning">Active but DNS not found</small>';
|
|
} else {
|
|
dkimStatus.innerHTML = '<span class="status-indicator" style="background-color: #6c757d;"></span><small class="text-muted">Disabled</small>';
|
|
}
|
|
spfStatus.innerHTML = spfResult.success
|
|
? '<span class="status-indicator status-success"></span><small class="text-success">Found</small>'
|
|
: '<span class="status-indicator status-danger"></span><small class="text-danger">Not found</small>';
|
|
showDNSResults(domain, dkimResult, spfResult);
|
|
} catch (error) {
|
|
dkimStatus.innerHTML = '<span class="status-indicator status-danger"></span><small class="text-danger">Error</small>';
|
|
spfStatus.innerHTML = '<span class="status-indicator status-danger"></span><small class="text-danger">Error</small>';
|
|
}
|
|
}
|
|
|
|
function showDNSResults(domain, dkimResult, spfResult) {
|
|
const resultsHtml = `
|
|
<h6>DNS Check Results for ${domain}</h6>
|
|
<div class="mb-3"><h6 class="text-primary">DKIM Record</h6>
|
|
<div class="alert ${dkimResult.success ? 'alert-success' : 'alert-danger'}">
|
|
<strong>Status:</strong> ${dkimResult.success ? 'Found' : 'Not Found'}<br>
|
|
<strong>Message:</strong> ${dkimResult.message}
|
|
</div>
|
|
</div>
|
|
<div class="mb-3"><h6 class="text-primary">SPF Record</h6>
|
|
<div class="alert ${spfResult.success ? 'alert-success' : 'alert-danger'}">
|
|
<strong>Status:</strong> ${spfResult.success ? 'Found' : 'Not Found'}<br>
|
|
<strong>Message:</strong> ${spfResult.message}
|
|
</div>
|
|
</div>`;
|
|
document.getElementById('dnsResults').innerHTML = resultsHtml;
|
|
new bootstrap.Modal(document.getElementById('dnsResultModal')).show();
|
|
}
|
|
|
|
async function checkAllDNS() {
|
|
const cards = document.querySelectorAll('[data-action="check-dns"]');
|
|
for (const btn of cards) {
|
|
await checkDomainDNS(btn.dataset.domain, btn.dataset.selector);
|
|
}
|
|
}
|
|
|
|
document.addEventListener('DOMContentLoaded', function() {
|
|
const checkAllBtn = document.querySelector('[data-action="check-all-dns"]');
|
|
if (checkAllBtn) { checkAllBtn.addEventListener('click', checkAllDNS); }
|
|
document.querySelectorAll('[data-action="check-dns"]').forEach(button => {
|
|
button.addEventListener('click', function(event) {
|
|
event.stopPropagation();
|
|
checkDomainDNS(this.dataset.domain, this.dataset.selector);
|
|
});
|
|
});
|
|
document.querySelectorAll('.card-header-clickable[data-bs-toggle="collapse"]').forEach(function(element) {
|
|
element.addEventListener('click', function() {
|
|
const targetId = this.getAttribute('data-bs-target');
|
|
const chevron = document.querySelector(targetId.replace('#collapse-', '#chevron-'));
|
|
if (chevron) {
|
|
setTimeout(() => {
|
|
const collapseElement = document.querySelector(targetId);
|
|
chevron.className = (collapseElement && collapseElement.classList.contains('show')) ? 'bi bi-chevron-up' : 'bi bi-chevron-down';
|
|
}, 100);
|
|
}
|
|
});
|
|
});
|
|
document.querySelectorAll('form[action*="toggle"]').forEach(form => {
|
|
if (!form.action.includes('/dkim/')) return;
|
|
form.addEventListener('submit', async function(event) {
|
|
event.preventDefault();
|
|
const response = await fetch(this.action, { method: 'POST', body: new FormData(this), headers: {'X-Requested-With': 'XMLHttpRequest'} });
|
|
if (response.ok) {
|
|
const result = await response.json();
|
|
if (result.success) { showToast(result.message, 'success'); setTimeout(() => location.reload(), 600); }
|
|
else { showToast(result.message, 'danger'); }
|
|
}
|
|
});
|
|
});
|
|
document.getElementById('createDKIMForm').addEventListener('submit', async function(event) {
|
|
event.preventDefault();
|
|
const response = await fetch(this.action, { method: 'POST', body: new FormData(this) });
|
|
const result = await response.json();
|
|
if (result.success) { showToast(result.message, 'success'); setTimeout(() => location.reload(), 600); }
|
|
else { showToast(result.message, 'danger'); }
|
|
});
|
|
});
|
|
</script>
|
|
{{end}}
|