2025-06-07 10:48:03 +01:00
{% extends "base.html" %}
{% block title %}DKIM Keys - Email Server{% endblock %}
{% block extra_css %}
< style >
.dns-record {
font-family: 'Courier New', monospace;
2025-06-07 11:57:21 +01:00
color: black;
2025-06-07 10:48:03 +01:00
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 >
{% endblock %}
{% block 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-info" onclick = "checkAllDNS()" >
< i class = "bi bi-arrow-clockwise me-2" > < / i >
Check All DNS
< / button >
< / div >
< / div >
{% for item in dkim_data %}
< div class = "card mb-4" id = "domain-{{ item.domain.id }}" >
2025-06-07 11:57:21 +01:00
< div class = "card-header" style = "cursor: pointer;" data-bs-toggle = "collapse" data-bs-target = "#collapse-{{ item.domain.id }}" aria-expanded = "false" aria-controls = "collapse-{{ item.domain.id }}" >
2025-06-07 10:48:03 +01:00
< div class = "d-flex justify-content-between align-items-center" >
< h5 class = "mb-0" >
< i class = "bi bi-server me-2" > < / i >
{{ item.domain.domain_name }}
{% if item.dkim_key.is_active %}
< span class = "badge bg-success ms-2" > Active< / span >
{% else %}
< span class = "badge bg-secondary ms-2" > Inactive< / span >
{% endif %}
< / h5 >
< div class = "btn-group btn-group-sm" >
< button class = "btn btn-outline-primary" onclick = "checkDomainDNS('{{ item.domain.domain_name }}', '{{ item.dkim_key.selector }}')" >
< i class = "bi bi-search me-1" > < / i >
Check DNS
< / button >
2025-06-07 11:57:21 +01:00
< div class = "btn-group btn-group-sm" role = "group" >
< a href = "{{ url_for('email.edit_dkim', dkim_id=item.dkim_key.id) }}"
class="btn btn-outline-info">
< i class = "bi bi-pencil me-1" > < / i >
Edit
< / a >
< form method = "post" action = "{{ url_for('email.toggle_dkim', dkim_id=item.dkim_key.id) }}" class = "d-inline" >
{% if item.dkim_key.is_active %}
< button type = "submit" class = "btn btn-outline-warning" >
< i class = "bi bi-pause-circle me-1" > < / i >
Disable
< / button >
{% else %}
< button type = "submit" class = "btn btn-outline-success" >
< i class = "bi bi-play-circle me-1" > < / i >
Enable
< / button >
{% endif %}
< / form >
< form method = "post" action = "{{ url_for('email.remove_dkim', dkim_id=item.dkim_key.id) }}" class = "d-inline" >
< button type = "submit"
class="btn btn-outline-danger"
onclick="return confirm('Are you sure you want to permanently remove the DKIM key for {{ item.domain.domain_name }}? This action cannot be undone and 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 >
2025-06-07 10:48:03 +01:00
< form method = "post" action = "{{ url_for('email.regenerate_dkim', domain_id=item.domain.id) }}" class = "d-inline" >
< button type = "submit"
class="btn btn-outline-warning"
onclick="return confirm('Regenerate DKIM key for {{ item.domain.domain_name }}? This will require updating DNS records.')">
< i class = "bi bi-arrow-clockwise me-1" > < / i >
Regenerate
< / button >
< / form >
< / div >
2025-06-07 11:57:21 +01:00
< div class = "ms-auto" >
< i class = "bi bi-chevron-down" id = "chevron-{{ item.domain.id }}" > < / i >
< / div >
2025-06-07 10:48:03 +01:00
< / div >
< / div >
2025-06-07 11:57:21 +01:00
< div class = "collapse" id = "collapse-{{ item.domain.id }}" >
< div class = "card-body" >
2025-06-07 10:48:03 +01:00
< div class = "row" >
<!-- DKIM DNS Record -->
< 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-{{ item.domain.id }}" >
< 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" > {{ item.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" > {{ item.dns_record.value }}< / div >
< / div >
< button class = "btn btn-outline-secondary btn-sm" onclick = "copyToClipboard('{{ item.dns_record.value }}')" >
< i class = "bi bi-clipboard me-1" > < / i >
Copy Value
< / button >
< / div >
<!-- SPF DNS Record -->
< 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-{{ item.domain.id }}" >
< 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" > {{ item.domain.domain_name }}< / div >
< / div >
< div class = "mb-2" >
< strong > Type:< / strong > TXT
< / div >
{% if item.existing_spf %}
< div class = "mb-2" >
< strong > Current SPF:< / strong >
< div class = "dns-record text-info" > {{ item.existing_spf }}< / div >
< / div >
{% endif %}
< div class = "mb-2" >
< strong > Recommended SPF:< / strong >
< div class = "dns-record text-success" > {{ item.recommended_spf }}< / div >
< / div >
< button class = "btn btn-outline-secondary btn-sm" onclick = "copyToClipboard('{{ item.recommended_spf }}')" >
< i class = "bi bi-clipboard me-1" > < / i >
Copy SPF
< / button >
< / div >
< / div >
<!-- Key Information -->
< 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 > {{ item.dkim_key.selector }}< / code >
< / div >
< div class = "col-md-3" >
< strong > Created:< / strong > < br >
{{ item.dkim_key.created_at.strftime('%Y-%m-%d %H:%M') }}
< / div >
< div class = "col-md-3" >
< strong > Server IP:< / strong > < br >
< code > {{ item.public_ip }}< / code >
< / div >
< div class = "col-md-3" >
< strong > Status:< / strong > < br >
{% if item.dkim_key.is_active %}
< span class = "text-success" > Active< / span >
{% else %}
< span class = "text-secondary" > Inactive< / span >
{% endif %}
< / div >
< / div >
< / div >
< / div >
2025-06-07 11:57:21 +01:00
< / div >
2025-06-07 10:48:03 +01:00
< / div >
< / div >
{% endfor %}
{% 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 = "{{ url_for('email.add_domain') }}" class = "btn btn-primary" >
< i class = "bi bi-plus-circle me-2" > < / i >
Add Domain
< / a >
< / div >
< / div >
{% endif %}
< / div >
<!-- DNS Check Results Modal -->
< 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" >
<!-- Results will be populated here -->
< / div >
< div class = "modal-footer" >
< button type = "button" class = "btn btn-secondary" data-bs-dismiss = "modal" > Close< / button >
< / div >
< / div >
< / div >
< / div >
{% endblock %}
{% block extra_js %}
< script >
async function checkDomainDNS(domain, selector) {
const dkimStatus = document.getElementById(`dkim-status-${domain.replace('.', '-')}`);
const spfStatus = document.getElementById(`spf-status-${domain.replace('.', '-')}`);
// Show loading state
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 {
// Check DKIM DNS
const dkimResponse = await fetch('{{ url_for("email.check_dkim_dns") }}', {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
},
body: `domain=${encodeURIComponent(domain)}& selector=${encodeURIComponent(selector)}`
});
const dkimResult = await dkimResponse.json();
// Check SPF DNS
const spfResponse = await fetch('{{ url_for("email.check_spf_dns") }}', {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
},
body: `domain=${encodeURIComponent(domain)}`
});
const spfResult = await spfResponse.json();
// Update DKIM status
if (dkimResult.success) {
dkimStatus.innerHTML = '< span class = "status-indicator status-success" > < / span > < small class = "text-success" > ✓ Configured< / small > ';
} else {
dkimStatus.innerHTML = '< span class = "status-indicator status-danger" > < / span > < small class = "text-danger" > ✗ Not found< / small > ';
}
// Update SPF status
if (spfResult.success) {
spfStatus.innerHTML = '< span class = "status-indicator status-success" > < / span > < small class = "text-success" > ✓ Found< / small > ';
} else {
spfStatus.innerHTML = '< span class = "status-indicator status-danger" > < / span > < small class = "text-danger" > ✗ Not found< / small > ';
}
// Show detailed results in modal
showDNSResults(domain, dkimResult, spfResult);
} catch (error) {
console.error('DNS check error:', 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}
${dkimResult.records ? `< br > < strong > Records:< / strong > ${dkimResult.records.join(', ')}` : ''}
< / 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}
${spfResult.spf_record ? `< br > < strong > Current SPF:< / strong > < code > ${spfResult.spf_record}< / code > ` : ''}
< / div >
< / div >
`;
document.getElementById('dnsResults').innerHTML = resultsHtml;
new bootstrap.Modal(document.getElementById('dnsResultModal')).show();
}
async function checkAllDNS() {
const domains = document.querySelectorAll('[id^="domain-"]');
for (const domainCard of domains) {
const domainId = domainCard.id.split('-')[1];
2025-06-07 11:57:21 +01:00
// Extract domain name from the card header
const domainHeaderText = domainCard.querySelector('h5').textContent.trim();
const domainName = domainHeaderText.split('\n')[0].trim().replace(/^\s*\S+\s+/, ''); // Remove icon
2025-06-07 10:48:03 +01:00
const selectorElement = domainCard.querySelector('code');
if (selectorElement) {
const selector = selectorElement.textContent;
await checkDomainDNS(domainName, selector);
// Small delay between checks to avoid overwhelming the DNS server
await new Promise(resolve => setTimeout(resolve, 500));
}
}
}
function copyToClipboard(text) {
navigator.clipboard.writeText(text).then(() => {
// Show temporary success message
const button = event.target.closest('button');
const originalText = button.innerHTML;
button.innerHTML = '< i class = "bi bi-check me-1" > < / i > Copied!';
button.classList.add('btn-success');
button.classList.remove('btn-outline-secondary');
setTimeout(() => {
button.innerHTML = originalText;
button.classList.remove('btn-success');
button.classList.add('btn-outline-secondary');
}, 2000);
}).catch(err => {
console.error('Failed to copy: ', err);
});
}
2025-06-07 11:57:21 +01:00
// Handle collapsible cards
document.addEventListener('DOMContentLoaded', function() {
// Add click handlers for card headers
document.querySelectorAll('[data-bs-toggle="collapse"]').forEach(function(element) {
element.addEventListener('click', function() {
const targetId = this.getAttribute('data-bs-target');
const chevronId = targetId.replace('#collapse-', '#chevron-');
const chevron = document.querySelector(chevronId);
// Toggle chevron direction
if (chevron) {
setTimeout(() => {
const collapseElement = document.querySelector(targetId);
if (collapseElement & & collapseElement.classList.contains('show')) {
chevron.className = 'bi bi-chevron-up';
} else {
chevron.className = 'bi bi-chevron-down';
}
}, 100);
}
});
});
});
2025-06-07 10:48:03 +01:00
< / script >
{% endblock %}