update the website code files, fix dns check for DKIM
This commit is contained in:
@@ -82,7 +82,7 @@
|
||||
</div>
|
||||
|
||||
<div class="d-flex justify-content-between">
|
||||
<a href="{{ url_for('email.users_list') }}" class="btn btn-secondary">
|
||||
<a href="{{ url_for('email.senders_list') }}" class="btn btn-secondary">
|
||||
<i class="bi bi-arrow-left me-2"></i>
|
||||
Back to Senders
|
||||
</a>
|
||||
@@ -246,7 +246,7 @@
|
||||
</div>
|
||||
<div class="col-md-3 mb-3">
|
||||
<div class="d-grid">
|
||||
<a href="{{ url_for('email.add_user') }}" class="btn btn-outline-success">
|
||||
<a href="{{ url_for('email.add_sender') }}" class="btn btn-outline-success">
|
||||
<i class="bi bi-person-plus me-2"></i>
|
||||
Add User
|
||||
</a>
|
||||
|
||||
@@ -38,7 +38,7 @@
|
||||
<i class="bi bi-plus-circle me-2"></i>
|
||||
Create DKIM
|
||||
</button>
|
||||
<button class="btn btn-outline-info" onclick="checkAllDNS()">
|
||||
<button class="btn btn-outline-info" data-action="check-all-dns">
|
||||
<i class="bi bi-arrow-clockwise me-2"></i>
|
||||
Check All DNS
|
||||
</button>
|
||||
@@ -81,10 +81,10 @@
|
||||
</div>
|
||||
|
||||
{% for item in dkim_data %}
|
||||
<div class="card mb-4" id="domain-{{ item.domain.id }}">
|
||||
<div class="card mb-4" id="domain-{{ item.domain.domain_name.replace('.', '-') }}" data-is-active="{{ item.dkim_key.is_active|tojson }}">
|
||||
<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-{{ item.domain.id }}" aria-expanded="false" aria-controls="collapse-{{ item.domain.id }}">
|
||||
<div class="flex-grow-1 card-header-clickable" style="cursor: pointer;" data-bs-toggle="collapse" data-bs-target="#collapse-{{ item.domain.domain_name.replace('.', '-') }}" aria-expanded="false" aria-controls="collapse-{{ item.domain.domain_name.replace('.', '-') }}">
|
||||
<h5 class="mb-0">
|
||||
<i class="bi bi-server me-2"></i>
|
||||
{{ item.domain.domain_name }}
|
||||
@@ -96,7 +96,11 @@
|
||||
</h5>
|
||||
</div>
|
||||
<div class="btn-group btn-group-sm me-2">
|
||||
<button class="btn btn-outline-primary" onclick="event.stopPropagation(); checkDomainDNS('{{ item.domain.domain_name }}', '{{ item.dkim_key.selector }}')">
|
||||
<button class="btn btn-outline-primary"
|
||||
data-action="check-dns"
|
||||
data-domain="{{ item.domain.domain_name }}"
|
||||
data-selector="{{ item.dkim_key.selector }}"
|
||||
onclick="event.stopPropagation();">
|
||||
<i class="bi bi-search me-1"></i>
|
||||
Check DNS
|
||||
</button>
|
||||
@@ -109,12 +113,12 @@
|
||||
</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" onclick="event.stopPropagation();">
|
||||
<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();">
|
||||
<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>
|
||||
@@ -135,12 +139,12 @@
|
||||
Regenerate
|
||||
</button>
|
||||
</div>
|
||||
<div class="card-header-clickable" style="cursor: pointer;" data-bs-toggle="collapse" data-bs-target="#collapse-{{ item.domain.id }}" aria-expanded="false" aria-controls="collapse-{{ item.domain.id }}">
|
||||
<i class="bi bi-chevron-down" id="chevron-{{ item.domain.id }}"></i>
|
||||
<div class="card-header-clickable" style="cursor: pointer;" data-bs-toggle="collapse" data-bs-target="#collapse-{{ item.domain.domain_name.replace('.', '-') }}" aria-expanded="false" aria-controls="collapse-{{ item.domain.domain_name.replace('.', '-') }}">
|
||||
<i class="bi bi-chevron-down" id="chevron-{{ item.domain.domain_name.replace('.', '-') }}"></i>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="collapse" id="collapse-{{ item.domain.id }}">
|
||||
<div class="collapse" id="collapse-{{ item.domain.domain_name.replace('.', '-') }}">
|
||||
<div class="card-body">
|
||||
<div class="row">
|
||||
<!-- DKIM DNS Record -->
|
||||
@@ -150,7 +154,7 @@
|
||||
DKIM DNS Record
|
||||
<span class="dns-status" id="dkim-status-{{ item.domain.domain_name.replace('.', '-') }}">
|
||||
<span class="status-indicator status-warning"></span>
|
||||
<small class="text-muted">Not checked</small>
|
||||
<small class="text-muted">Active (DNS not checked)</small>
|
||||
</span>
|
||||
</h6>
|
||||
<div class="mb-2">
|
||||
@@ -327,6 +331,56 @@
|
||||
|
||||
{% block extra_js %}
|
||||
<script>
|
||||
// Show toast notification
|
||||
function showToast(message, type = 'info') {
|
||||
const toastContainer = document.getElementById('toastContainer');
|
||||
if (!toastContainer) {
|
||||
// Create toast container if it doesn't exist
|
||||
const container = document.createElement('div');
|
||||
container.id = 'toastContainer';
|
||||
container.style.cssText = 'position: fixed; top: 20px; right: 20px; z-index: 1050;';
|
||||
document.body.appendChild(container);
|
||||
}
|
||||
|
||||
const toast = document.createElement('div');
|
||||
toast.className = `toast align-items-center border-0 bg-${type}`;
|
||||
toast.setAttribute('role', 'alert');
|
||||
toast.setAttribute('aria-live', 'assertive');
|
||||
toast.setAttribute('aria-atomic', 'true');
|
||||
|
||||
const toastContent = document.createElement('div');
|
||||
toastContent.className = 'd-flex';
|
||||
|
||||
const toastBody = document.createElement('div');
|
||||
toastBody.className = 'toast-body text-white';
|
||||
toastBody.textContent = message;
|
||||
|
||||
const closeButton = document.createElement('button');
|
||||
closeButton.type = 'button';
|
||||
closeButton.className = 'btn-close btn-close-white me-2 m-auto';
|
||||
closeButton.setAttribute('data-bs-dismiss', 'toast');
|
||||
closeButton.setAttribute('aria-label', 'Close');
|
||||
|
||||
toastContent.appendChild(toastBody);
|
||||
toastContent.appendChild(closeButton);
|
||||
toast.appendChild(toastContent);
|
||||
|
||||
document.getElementById('toastContainer').appendChild(toast);
|
||||
|
||||
const bsToast = new bootstrap.Toast(toast, {
|
||||
animation: true,
|
||||
autohide: true,
|
||||
delay: 5000
|
||||
});
|
||||
bsToast.show();
|
||||
|
||||
// Remove toast after it's hidden
|
||||
toast.addEventListener('hidden.bs.toast', function() {
|
||||
toast.remove();
|
||||
});
|
||||
}
|
||||
|
||||
// Check DNS records for a domain
|
||||
async function checkDomainDNS(domain, selector) {
|
||||
const dkimStatus = document.getElementById(`dkim-status-${domain.replace('.', '-')}`);
|
||||
const spfStatus = document.getElementById(`spf-status-${domain.replace('.', '-')}`);
|
||||
@@ -337,30 +391,43 @@
|
||||
|
||||
try {
|
||||
// Check DKIM DNS
|
||||
const dkimResponse = await fetch('{{ url_for("email.check_dkim_dns") }}', {
|
||||
const dkimResponse = await fetch("{{ url_for('email.check_dkim_dns') }}", {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/x-www-form-urlencoded',
|
||||
'Content-Type': 'application/x-www-form-urlencoded'
|
||||
},
|
||||
body: `domain=${encodeURIComponent(domain)}&selector=${encodeURIComponent(selector)}`
|
||||
body: new URLSearchParams({
|
||||
domain: domain,
|
||||
selector: selector
|
||||
})
|
||||
});
|
||||
const dkimResult = await dkimResponse.json();
|
||||
|
||||
// Check SPF DNS
|
||||
const spfResponse = await fetch('{{ url_for("email.check_spf_dns") }}', {
|
||||
const spfResponse = await fetch("{{ url_for('email.check_spf_dns') }}", {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/x-www-form-urlencoded',
|
||||
'Content-Type': 'application/x-www-form-urlencoded'
|
||||
},
|
||||
body: `domain=${encodeURIComponent(domain)}`
|
||||
body: new URLSearchParams({
|
||||
domain: 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>';
|
||||
// Get DKIM key status from data attribute
|
||||
const domainCard = document.getElementById(`domain-${domain.replace('.', '-')}`);
|
||||
const isActive = domainCard && domainCard.dataset.isActive === 'true';
|
||||
|
||||
// Update DKIM status based on active state and DNS visibility
|
||||
if (isActive) {
|
||||
if (dkimResult.success) {
|
||||
dkimStatus.innerHTML = '<span class="status-indicator status-success"></span><small class="text-success">✓ Active & Configured</small>';
|
||||
} else {
|
||||
dkimStatus.innerHTML = '<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 status-danger"></span><small class="text-danger">✗ Not found</small>';
|
||||
dkimStatus.innerHTML = '<span class="status-indicator" style="background-color: #6c757d;"></span><small class="text-muted">Disabled</small>';
|
||||
}
|
||||
|
||||
// Update SPF status
|
||||
@@ -380,7 +447,30 @@
|
||||
}
|
||||
}
|
||||
|
||||
// Show DNS check results in modal
|
||||
function showDNSResults(domain, dkimResult, spfResult) {
|
||||
// Clean up record strings by removing extra quotes and normalizing whitespace
|
||||
function cleanRecordDisplay(record) {
|
||||
if (!record) return '';
|
||||
return record
|
||||
.replace(/^["']|["']$/g, '') // Remove outer quotes
|
||||
.replace(/\\n/g, '') // Remove newlines
|
||||
.replace(/\s+/g, ' ') // Normalize whitespace
|
||||
.trim(); // Remove leading/trailing space
|
||||
}
|
||||
|
||||
const dkimRecordsHtml = dkimResult.records ?
|
||||
dkimResult.records.map(record =>
|
||||
`<div class="record-value" style="word-break: break-all; font-family: monospace; background: #f8f9fa; padding: 8px; border-radius: 4px;">
|
||||
${cleanRecordDisplay(record)}
|
||||
</div>`
|
||||
).join('') : '';
|
||||
|
||||
const spfRecordHtml = spfResult.spf_record ?
|
||||
`<div class="record-value mt-2" style="word-break: break-all; font-family: monospace; background: #f8f9fa; padding: 8px; border-radius: 4px;">
|
||||
${cleanRecordDisplay(spfResult.spf_record)}
|
||||
</div>` : '';
|
||||
|
||||
const resultsHtml = `
|
||||
<h6>DNS Check Results for ${domain}</h6>
|
||||
|
||||
@@ -389,7 +479,12 @@
|
||||
<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(', ')}` : ''}
|
||||
${dkimResult.records ? `
|
||||
<br><strong>Records:</strong>
|
||||
<div class="records-container mt-2">
|
||||
${dkimRecordsHtml}
|
||||
</div>
|
||||
` : ''}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -398,68 +493,10 @@
|
||||
<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();
|
||||
}
|
||||
|
||||
function showAllDNSResults(results) {
|
||||
let tableRows = '';
|
||||
|
||||
results.forEach(result => {
|
||||
const dkimIcon = result.dkim.success ? '<i class="bi bi-check-circle-fill text-success"></i>' : '<i class="bi bi-x-circle-fill text-danger"></i>';
|
||||
const spfIcon = result.spf.success ? '<i class="bi bi-check-circle-fill text-success"></i>' : '<i class="bi bi-x-circle-fill text-danger"></i>';
|
||||
|
||||
tableRows += `
|
||||
<tr>
|
||||
<td><strong>${result.domain}</strong></td>
|
||||
<td class="text-center">
|
||||
${dkimIcon}
|
||||
<small class="d-block">${result.dkim.success ? 'Configured' : 'Not Found'}</small>
|
||||
</td>
|
||||
<td class="text-center">
|
||||
${spfIcon}
|
||||
<small class="d-block">${result.spf.success ? 'Found' : 'Not Found'}</small>
|
||||
</td>
|
||||
<td>
|
||||
<small class="text-muted">
|
||||
DKIM: ${result.dkim.message}<br>
|
||||
SPF: ${result.spf.message}
|
||||
</small>
|
||||
</td>
|
||||
</tr>
|
||||
`;
|
||||
});
|
||||
|
||||
const resultsHtml = `
|
||||
<h6>DNS Check Results for All Domains</h6>
|
||||
<div class="table-responsive">
|
||||
<table class="table table-striped">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Domain</th>
|
||||
<th class="text-center">DKIM Status</th>
|
||||
<th class="text-center">SPF Status</th>
|
||||
<th>Details</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
${tableRows}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div class="mt-3">
|
||||
<div class="alert alert-info">
|
||||
<small>
|
||||
<i class="bi bi-info-circle me-1"></i>
|
||||
<strong>DKIM:</strong> Verifies email signatures for authenticity<br>
|
||||
<i class="bi bi-info-circle me-1"></i>
|
||||
<strong>SPF:</strong> Authorizes servers that can send email for your domain
|
||||
</small>
|
||||
${spfResult.spf_record ? `
|
||||
<br><strong>Current SPF:</strong>
|
||||
${spfRecordHtml}
|
||||
` : ''}
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
@@ -468,6 +505,7 @@
|
||||
new bootstrap.Modal(document.getElementById('dnsResultModal')).show();
|
||||
}
|
||||
|
||||
// Check all domains' DNS records
|
||||
async function checkAllDNS() {
|
||||
const domains = document.querySelectorAll('[id^="domain-"]');
|
||||
const results = [];
|
||||
@@ -544,100 +582,88 @@
|
||||
showAllDNSResults(results);
|
||||
}
|
||||
|
||||
// AJAX DKIM regeneration function
|
||||
// Simple DKIM regeneration function with page reload
|
||||
async function regenerateDKIM(domainId, domainName) {
|
||||
const confirmed = await showConfirmation(
|
||||
`Regenerate DKIM key for ${domainName}? This will require updating DNS records.`,
|
||||
'Regenerate DKIM Key',
|
||||
'Regenerate',
|
||||
'btn-warning'
|
||||
);
|
||||
// Show combined DNS check results
|
||||
function showAllDNSResults(results) {
|
||||
let tableRows = '';
|
||||
|
||||
if (!confirmed) {
|
||||
return;
|
||||
}
|
||||
|
||||
const button = event.target.closest('button');
|
||||
const originalContent = button.innerHTML;
|
||||
|
||||
// Show loading state
|
||||
button.innerHTML = '<i class="bi bi-arrow-clockwise spinner-border spinner-border-sm me-1"></i>Regenerating...';
|
||||
button.disabled = true;
|
||||
|
||||
try {
|
||||
const response = await fetch(`{{ url_for('email.regenerate_dkim', domain_id=0) }}`.replace('0', domainId), {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({})
|
||||
});
|
||||
results.forEach(result => {
|
||||
const dkimIcon = result.dkim.success ? '<i class="bi bi-check-circle-fill text-success"></i>' : '<i class="bi bi-x-circle-fill text-danger"></i>';
|
||||
const spfIcon = result.spf.success ? '<i class="bi bi-check-circle-fill text-success"></i>' : '<i class="bi bi-x-circle-fill text-danger"></i>';
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
if (result.success) {
|
||||
showToast('DKIM key regenerated successfully! Reloading page...', 'success');
|
||||
|
||||
// Reload the page after a short delay to show the success message
|
||||
setTimeout(() => {
|
||||
window.location.reload();
|
||||
}, 1500);
|
||||
} else {
|
||||
showToast(result.message || 'Error regenerating DKIM key', 'danger');
|
||||
// Restore button on error
|
||||
button.innerHTML = originalContent;
|
||||
button.disabled = false;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('DKIM regeneration error:', error);
|
||||
showToast('Error regenerating DKIM key', 'danger');
|
||||
// Restore button on error
|
||||
button.innerHTML = originalContent;
|
||||
button.disabled = false;
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
tableRows += `
|
||||
<tr>
|
||||
<td><strong>${result.domain}</strong></td>
|
||||
<td class="text-center">
|
||||
${dkimIcon}
|
||||
<small class="d-block">${result.dkim.success ? 'Configured' : 'Not Found'}</small>
|
||||
</td>
|
||||
<td class="text-center">
|
||||
${spfIcon}
|
||||
<small class="d-block">${result.spf.success ? 'Found' : 'Not Found'}</small>
|
||||
</td>
|
||||
<td>
|
||||
<small class="text-muted">
|
||||
DKIM: ${result.dkim.message}<br>
|
||||
SPF: ${result.spf.message}
|
||||
</small>
|
||||
</td>
|
||||
</tr>
|
||||
`;
|
||||
});
|
||||
|
||||
const resultsHtml = `
|
||||
<h6>DNS Check Results for All Domains</h6>
|
||||
<div class="table-responsive">
|
||||
<table class="table table-striped">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Domain</th>
|
||||
<th class="text-center">DKIM Status</th>
|
||||
<th class="text-center">SPF Status</th>
|
||||
<th>Details</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
${tableRows}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div class="mt-3">
|
||||
<div class="alert alert-info">
|
||||
<small>
|
||||
<i class="bi bi-info-circle me-1"></i>
|
||||
<strong>DKIM:</strong> Verifies email signatures for authenticity<br>
|
||||
<i class="bi bi-info-circle me-1"></i>
|
||||
<strong>SPF:</strong> Authorizes servers that can send email for your domain
|
||||
</small>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
document.getElementById('dnsResults').innerHTML = resultsHtml;
|
||||
new bootstrap.Modal(document.getElementById('dnsResultModal')).show();
|
||||
}
|
||||
|
||||
// Handle form submissions with custom confirmation dialogs
|
||||
async function handleFormSubmit(event, message) {
|
||||
event.preventDefault(); // Prevent default form submission
|
||||
|
||||
const confirmed = await showConfirmation(
|
||||
message,
|
||||
'Confirm Action',
|
||||
'Confirm',
|
||||
'btn-danger'
|
||||
);
|
||||
|
||||
if (confirmed) {
|
||||
// Submit the form if confirmed
|
||||
event.target.submit();
|
||||
}
|
||||
|
||||
return false; // Always return false to prevent default submission
|
||||
}
|
||||
|
||||
// Handle collapsible cards
|
||||
|
||||
// Initialize event handlers
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
// Add click handler for DNS check buttons
|
||||
document.querySelectorAll('[data-action="check-dns"]').forEach(button => {
|
||||
button.addEventListener('click', function(event) {
|
||||
event.stopPropagation();
|
||||
const domain = this.dataset.domain;
|
||||
const selector = this.dataset.selector;
|
||||
checkDomainDNS(domain, selector);
|
||||
});
|
||||
});
|
||||
|
||||
// Add click handler for check all DNS button
|
||||
const checkAllButton = document.querySelector('[data-action="check-all-dns"]');
|
||||
if (checkAllButton) {
|
||||
checkAllButton.addEventListener('click', function() {
|
||||
checkAllDNS();
|
||||
});
|
||||
}
|
||||
|
||||
// Add click handlers for card headers - only for clickable areas
|
||||
document.querySelectorAll('.card-header-clickable[data-bs-toggle="collapse"]').forEach(function(element) {
|
||||
element.addEventListener('click', function() {
|
||||
@@ -658,43 +684,78 @@
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
document.getElementById('createDKIMForm').addEventListener('submit', async function(event) {
|
||||
event.preventDefault();
|
||||
const domain = document.getElementById('dkimDomain').value;
|
||||
const selector = document.getElementById('dkimSelector').value.trim();
|
||||
const errorDiv = document.getElementById('createDKIMError');
|
||||
const successDiv = document.getElementById('createDKIMSuccess');
|
||||
errorDiv.classList.add('d-none');
|
||||
successDiv.classList.add('d-none');
|
||||
if (!domain) {
|
||||
errorDiv.textContent = 'Please select a domain.';
|
||||
errorDiv.classList.remove('d-none');
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const response = await fetch("{{ url_for('email.create_dkim') }}", {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'X-Requested-With': 'XMLHttpRequest'
|
||||
},
|
||||
body: JSON.stringify({ domain, selector })
|
||||
// Add submit handler for DKIM toggle forms
|
||||
document.querySelectorAll('form[action*="toggle_dkim"]').forEach(form => {
|
||||
form.addEventListener('submit', async function(event) {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
|
||||
const formData = new FormData(this);
|
||||
const response = await fetch(this.action, {
|
||||
method: 'POST',
|
||||
body: formData,
|
||||
headers: {
|
||||
'X-Requested-With': 'XMLHttpRequest'
|
||||
}
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
const result = await response.json();
|
||||
if (result.success) {
|
||||
// Get the domain card
|
||||
const domainCard = this.closest('.card');
|
||||
if (domainCard) {
|
||||
// Update the data-is-active attribute
|
||||
domainCard.dataset.isActive = result.is_active.toString();
|
||||
|
||||
// Update the badge
|
||||
const badge = domainCard.querySelector('.badge');
|
||||
if (badge) {
|
||||
if (result.is_active) {
|
||||
badge.className = 'badge bg-success ms-2';
|
||||
badge.textContent = 'Active';
|
||||
} else {
|
||||
badge.className = 'badge bg-secondary ms-2';
|
||||
badge.textContent = 'Inactive';
|
||||
}
|
||||
}
|
||||
|
||||
// Update the button
|
||||
const button = this.querySelector('button');
|
||||
if (button) {
|
||||
if (result.is_active) {
|
||||
button.className = 'btn btn-outline-warning';
|
||||
button.innerHTML = '<i class="bi bi-pause-circle me-1"></i>Disable';
|
||||
button.title = 'Disable DKIM';
|
||||
} else {
|
||||
button.className = 'btn btn-outline-success';
|
||||
button.innerHTML = '<i class="bi bi-play-circle me-1"></i>Enable';
|
||||
button.title = 'Enable DKIM';
|
||||
}
|
||||
}
|
||||
|
||||
// Update the status indicator
|
||||
const dkimStatus = domainCard.querySelector('.dns-status');
|
||||
if (dkimStatus) {
|
||||
if (result.is_active) {
|
||||
dkimStatus.innerHTML = '<span class="status-indicator status-warning"></span><small class="text-muted">Active (DNS not checked)</small>';
|
||||
} else {
|
||||
dkimStatus.innerHTML = '<span class="status-indicator" style="background-color: #6c757d;"></span><small class="text-muted">Disabled</small>';
|
||||
}
|
||||
}
|
||||
|
||||
// Show success message
|
||||
showToast(result.message, 'success');
|
||||
}
|
||||
} else {
|
||||
showToast(result.message, 'error');
|
||||
}
|
||||
} else {
|
||||
showToast('Error toggling DKIM status', 'error');
|
||||
}
|
||||
});
|
||||
const result = await response.json();
|
||||
if (result.success) {
|
||||
successDiv.textContent = result.message || 'DKIM key created.';
|
||||
successDiv.classList.remove('d-none');
|
||||
setTimeout(() => { window.location.reload(); }, 1200);
|
||||
} else {
|
||||
errorDiv.textContent = result.message || 'Failed to create DKIM key.';
|
||||
errorDiv.classList.remove('d-none');
|
||||
}
|
||||
} catch (err) {
|
||||
errorDiv.textContent = 'Error creating DKIM key.';
|
||||
errorDiv.classList.remove('d-none');
|
||||
}
|
||||
});
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
||||
|
||||
@@ -31,7 +31,7 @@
|
||||
<th>Domain Name</th>
|
||||
<th>Status</th>
|
||||
<th>Created</th>
|
||||
<th>Users</th>
|
||||
<th>Senders</th>
|
||||
<th>DKIM</th>
|
||||
<th>Actions</th>
|
||||
</tr>
|
||||
@@ -62,19 +62,26 @@
|
||||
</td>
|
||||
<td>
|
||||
<span class="badge bg-info">
|
||||
{{ domain.users|length if domain.users else 0 }} users
|
||||
{{ domain.users|length }} senders
|
||||
</span>
|
||||
</td>
|
||||
<td>
|
||||
{% set has_dkim = domain.dkim_keys and domain.dkim_keys|selectattr('is_active')|list %}
|
||||
{% if has_dkim %}
|
||||
<span class="text-success">
|
||||
<i class="bi bi-shield-check" title="DKIM Configured"></i>
|
||||
{% set active_dkim_keys = domain.dkim_keys|selectattr('is_active')|list %}
|
||||
{% if active_dkim_keys %}
|
||||
<span class="dns-status" id="dkim-status-{{ domain.domain_name.replace('.', '-') }}">
|
||||
<span class="status-indicator status-warning"></span>
|
||||
<i class="bi bi-shield-check" title="DKIM Active (DNS not checked)"></i>
|
||||
</span>
|
||||
{% else %}
|
||||
<span class="text-warning">
|
||||
<i class="bi bi-shield-exclamation" title="No DKIM Key"></i>
|
||||
</span>
|
||||
{% if domain.dkim_keys|length > 0 %}
|
||||
<span class="text-secondary">
|
||||
<i class="bi bi-shield" title="DKIM Disabled"></i>
|
||||
</span>
|
||||
{% else %}
|
||||
<span class="text-danger">
|
||||
<i class="bi bi-shield-exclamation" title="No DKIM Key"></i>
|
||||
</span>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>
|
||||
@@ -153,11 +160,15 @@
|
||||
</li>
|
||||
<li class="mb-2">
|
||||
<i class="bi bi-shield-check text-warning me-2"></i>
|
||||
<strong>DKIM configured:</strong> {{ domains|selectattr('dkim_keys')|list|length }}
|
||||
</li>
|
||||
<li>
|
||||
<i class="bi bi-people text-info me-2"></i>
|
||||
<strong>Total users:</strong> {{ domains|sum(attribute='users')|length if domains[0].users is defined else 'N/A' }}
|
||||
<strong>DKIM configured:</strong>
|
||||
{% set dkim_count = namespace(active=0) %}
|
||||
{% for domain in domains %}
|
||||
{% set active_dkim_keys = domain.dkim_keys|selectattr('is_active')|list %}
|
||||
{% if active_dkim_keys %}
|
||||
{% set dkim_count.active = dkim_count.active + 1 %}
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
{{ dkim_count.active }}
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
@@ -183,7 +194,7 @@
|
||||
</li>
|
||||
<li>
|
||||
<i class="bi bi-arrow-right text-primary me-2"></i>
|
||||
Add users or whitelist IPs for authentication
|
||||
Add senders or whitelist IPs for authentication
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
@@ -192,3 +203,73 @@
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
|
||||
{% block extra_js %}
|
||||
<script>
|
||||
async function checkDomainDKIM(domain, selector) {
|
||||
const dkimStatus = document.getElementById(`dkim-status-${domain.replace('.', '-')}`);
|
||||
if (!dkimStatus) return;
|
||||
|
||||
try {
|
||||
// Check DKIM DNS
|
||||
const response = await fetch("{{ url_for('email.check_dkim_dns') }}", {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/x-www-form-urlencoded'
|
||||
},
|
||||
body: new URLSearchParams({
|
||||
domain: domain,
|
||||
selector: selector
|
||||
})
|
||||
});
|
||||
const result = await response.json();
|
||||
|
||||
if (result.success) {
|
||||
dkimStatus.innerHTML = `
|
||||
<span class="status-indicator status-success"></span>
|
||||
<i class="bi bi-shield-check" title="DKIM Active & DNS Configured"></i>
|
||||
`;
|
||||
} else {
|
||||
dkimStatus.innerHTML = `
|
||||
<span class="status-indicator" style="background-color: #fd7e14;"></span>
|
||||
<i class="bi bi-shield-exclamation" title="DKIM Active but DNS not found"></i>
|
||||
`;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('DKIM DNS check error:', error);
|
||||
dkimStatus.innerHTML = `
|
||||
<span class="status-indicator status-danger"></span>
|
||||
<i class="bi bi-shield-x" title="Error checking DKIM DNS"></i>
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
// Check DKIM DNS for all domains when page loads
|
||||
document.addEventListener('DOMContentLoaded', async function() {
|
||||
{% for domain in domains %}
|
||||
{% set active_dkim_keys = domain.dkim_keys|selectattr('is_active')|list %}
|
||||
{% if active_dkim_keys %}
|
||||
{% set active_key = active_dkim_keys|first %}
|
||||
await checkDomainDKIM('{{ domain.domain_name }}', '{{ active_key.selector }}');
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
});
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.status-indicator {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
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; }
|
||||
.dns-status {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
}
|
||||
</style>
|
||||
{% endblock %}
|
||||
|
||||
@@ -70,7 +70,7 @@
|
||||
<i class="bi bi-check-lg me-1"></i>
|
||||
Update User
|
||||
</button>
|
||||
<a href="{{ url_for('email.users_list') }}" class="btn btn-secondary">
|
||||
<a href="{{ url_for('email.senders_list') }}" class="btn btn-secondary">
|
||||
<i class="bi bi-x-lg me-1"></i>
|
||||
Cancel
|
||||
</a>
|
||||
@@ -75,7 +75,7 @@
|
||||
|
||||
<!-- Enable/Disable Button -->
|
||||
{% if ip.is_active %}
|
||||
<form method="post" action="{{ url_for('email.delete_ip', ip_id=ip.id) }}" class="d-inline">
|
||||
<form method="post" action="{{ url_for('email.disable_ip', ip_id=ip.id) }}" class="d-inline">
|
||||
<button type="submit"
|
||||
class="btn btn-outline-warning btn-sm"
|
||||
title="Disable IP"
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
<i class="bi bi-people me-2"></i>
|
||||
Senders
|
||||
</h2>
|
||||
<a href="{{ url_for('email.add_user') }}" class="btn btn-primary">
|
||||
<a href="{{ url_for('email.add_sender') }}" class="btn btn-primary">
|
||||
<i class="bi bi-person-plus me-2"></i>
|
||||
Add Sender
|
||||
</a>
|
||||
@@ -136,7 +136,7 @@
|
||||
<td>
|
||||
<div class="btn-group" role="group">
|
||||
<!-- Edit Button -->
|
||||
<a href="{{ url_for('email.edit_user', user_id=user.id) }}"
|
||||
<a href="{{ url_for('email.edit_sender', user_id=user.id) }}"
|
||||
class="btn btn-outline-primary btn-sm"
|
||||
title="Edit Sender">
|
||||
<i class="bi bi-pencil"></i>
|
||||
@@ -144,7 +144,7 @@
|
||||
|
||||
<!-- Enable/Disable Button -->
|
||||
{% if user.is_active %}
|
||||
<form method="post" action="{{ url_for('email.delete_user', user_id=user.id) }}" class="d-inline">
|
||||
<form method="post" action="{{ url_for('email.delete_sender', user_id=user.id) }}" class="d-inline">
|
||||
<button type="submit"
|
||||
class="btn btn-outline-warning btn-sm"
|
||||
title="Disable Sender"
|
||||
@@ -153,7 +153,7 @@
|
||||
</button>
|
||||
</form>
|
||||
{% else %}
|
||||
<form method="post" action="{{ url_for('email.enable_user', user_id=user.id) }}" class="d-inline">
|
||||
<form method="post" action="{{ url_for('email.enable_sender', user_id=user.id) }}" class="d-inline">
|
||||
<button type="submit"
|
||||
class="btn btn-outline-success btn-sm"
|
||||
title="Enable Sender"
|
||||
@@ -164,7 +164,7 @@
|
||||
{% endif %}
|
||||
|
||||
<!-- Permanent Remove Button -->
|
||||
<form method="post" action="{{ url_for('email.remove_user', user_id=user.id) }}" class="d-inline">
|
||||
<form method="post" action="{{ url_for('email.remove_sender', user_id=user.id) }}" class="d-inline">
|
||||
<button type="submit"
|
||||
class="btn btn-outline-danger btn-sm"
|
||||
title="Permanently Remove Sender"
|
||||
@@ -184,7 +184,7 @@
|
||||
<i class="bi bi-people text-muted" style="font-size: 4rem;"></i>
|
||||
<h4 class="text-muted mt-3">No senders configured</h4>
|
||||
<p class="text-muted">Add sender to enable username/password authentication</p>
|
||||
<a href="{{ url_for('email.add_user') }}" class="btn btn-primary">
|
||||
<a href="{{ url_for('email.add_sender') }}" class="btn btn-primary">
|
||||
<i class="bi bi-person-plus me-2"></i>
|
||||
Add Your First Sender
|
||||
</a>
|
||||
@@ -14,6 +14,15 @@
|
||||
color: var(--bs-secondary);
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
.card-header {
|
||||
cursor: pointer;
|
||||
}
|
||||
.card-header .bi-chevron-down {
|
||||
transition: transform 0.2s;
|
||||
}
|
||||
.card-header.collapsed .bi-chevron-down {
|
||||
transform: rotate(-90deg);
|
||||
}
|
||||
</style>
|
||||
{% endblock %}
|
||||
|
||||
@@ -40,61 +49,89 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<form method="POST" action="{{ url_for('email.update_settings') }}">
|
||||
<form method="POST" action="{{ url_for('email.settings_update') }}" id="settingsForm">
|
||||
<!-- Add CSRF token if enabled -->
|
||||
{% if csrf_token %}
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
|
||||
{% endif %}
|
||||
<!-- Server Settings -->
|
||||
<div class="card mb-4">
|
||||
<div class="card-header">
|
||||
<h5 class="mb-0">
|
||||
<i class="bi bi-server me-2"></i>
|
||||
Server Configuration
|
||||
<div class="card-header" data-bs-toggle="collapse" data-bs-target="#serverSettings" aria-expanded="true">
|
||||
<h5 class="mb-0 d-flex justify-content-between align-items-center">
|
||||
<span><i class="bi bi-server me-2"></i>Server Configuration</span>
|
||||
<i class="bi bi-chevron-down"></i>
|
||||
</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="setting-section">
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<div class="mb-3">
|
||||
<label class="form-label">SMTP Port</label>
|
||||
<div class="setting-description">Port for SMTP connections (standard: 25, 587)</div>
|
||||
<input type="number"
|
||||
class="form-control"
|
||||
name="Server.SMTP_PORT"
|
||||
value="{{ settings['Server']['SMTP_PORT'] }}"
|
||||
min="1" max="65535">
|
||||
<div id="serverSettings" class="collapse show">
|
||||
<div class="card-body">
|
||||
<div class="setting-section">
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<div class="mb-3">
|
||||
<label class="form-label">SMTP Port</label>
|
||||
<div class="setting-description">Port for SMTP unencrypted connections (standard: 25)</div>
|
||||
<input type="number"
|
||||
class="form-control"
|
||||
name="Server.smtp_port"
|
||||
value="{{ settings['Server']['smtp_port'] }}"
|
||||
min="1" max="65535">
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<div class="mb-3">
|
||||
<label class="form-label">SMTP STARTTLS Port</label>
|
||||
<div class="setting-description">Port for SMTP STARTTLS connections (standard: 587)</div>
|
||||
<input type="number"
|
||||
class="form-control"
|
||||
name="Server.smtp_tls_port"
|
||||
value="{{ settings['Server']['smtp_tls_port'] }}"
|
||||
min="1" max="65535">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<div class="mb-3">
|
||||
<label class="form-label">SMTP TLS Port</label>
|
||||
<div class="setting-description">Port for SMTP over TLS connections (standard: 465)</div>
|
||||
<input type="number"
|
||||
class="form-control"
|
||||
name="Server.SMTP_TLS_PORT"
|
||||
value="{{ settings['Server']['SMTP_TLS_PORT'] }}"
|
||||
min="1" max="65535">
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Bind IP Address</label>
|
||||
<div class="setting-description">IP address to bind SMTP server only to (0.0.0.0 for all interfaces)</div>
|
||||
<input type="text"
|
||||
class="form-control"
|
||||
name="Server.bind_ip"
|
||||
value="{{ settings['Server']['bind_ip'] }}"
|
||||
pattern="^(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$">
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Hostname</label>
|
||||
<div class="setting-description">Server hostname for HELO/EHLO commands</div>
|
||||
<input type="text"
|
||||
class="form-control"
|
||||
name="Server.hostname"
|
||||
value="{{ settings['Server']['hostname'] }}">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Bind IP Address</label>
|
||||
<div class="setting-description">IP address to bind the server to (0.0.0.0 for all interfaces)</div>
|
||||
<input type="text"
|
||||
class="form-control"
|
||||
name="Server.BIND_IP"
|
||||
value="{{ settings['Server']['BIND_IP'] }}"
|
||||
pattern="^(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$">
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<div class="mb-3">
|
||||
<label class="form-label">HELO Hostname</label>
|
||||
<div class="setting-description">Override HELO hostname for SMTP identification</div>
|
||||
<input type="text"
|
||||
class="form-control"
|
||||
name="Server.helo_hostname"
|
||||
value="{{ settings['Server']['helo_hostname'] }}">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Hostname</label>
|
||||
<div class="setting-description">Server hostname for HELO/EHLO commands</div>
|
||||
<input type="text"
|
||||
class="form-control"
|
||||
name="Server.hostname"
|
||||
value="{{ settings['Server']['hostname'] }}">
|
||||
<div class="col-md-6">
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Server Banner</label>
|
||||
<div class="setting-description">Custom SMTP server banner (empty by default - hides SMTP version)</div>
|
||||
<input type="text"
|
||||
class="form-control"
|
||||
name="Server.server_banner"
|
||||
value="{{ settings['Server']['server_banner'] }}">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -104,21 +141,47 @@
|
||||
|
||||
<!-- Database Settings -->
|
||||
<div class="card mb-4">
|
||||
<div class="card-header">
|
||||
<h5 class="mb-0">
|
||||
<i class="bi bi-database me-2"></i>
|
||||
Database Configuration
|
||||
<div class="card-header collapsed" data-bs-toggle="collapse" data-bs-target="#databaseSettings" aria-expanded="false">
|
||||
<h5 class="mb-0 d-flex justify-content-between align-items-center">
|
||||
<span><i class="bi bi-database me-2"></i>Database Configuration</span>
|
||||
<i class="bi bi-chevron-down"></i>
|
||||
</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="setting-section">
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Database URL</label>
|
||||
<div class="setting-description">SQLite database file path or connection string</div>
|
||||
<input type="text"
|
||||
class="form-control font-monospace"
|
||||
name="Database.DATABASE_URL"
|
||||
value="{{ settings['Database']['DATABASE_URL'] }}">
|
||||
<div id="databaseSettings" class="collapse">
|
||||
<div class="card-body">
|
||||
<div class="setting-section">
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Database URL</label>
|
||||
<div class="setting-description">Database connection string</div>
|
||||
<div class="input-group mb-2">
|
||||
<input type="text"
|
||||
class="form-control font-monospace"
|
||||
name="Database.database_url"
|
||||
id="databaseUrl"
|
||||
value="{{ settings['Database']['database_url'] }}">
|
||||
<button class="btn btn-primary" type="button" onclick="testDatabaseConnection()">
|
||||
<i class="bi bi-check-circle me-1"></i>
|
||||
Test Connection
|
||||
</button>
|
||||
</div>
|
||||
<div class="mt-3">
|
||||
<label class="form-label">Example Connection Strings:</label>
|
||||
<div class="list-group">
|
||||
<button type="button" class="list-group-item list-group-item-action" onclick="setDatabaseExample('sqlite')">
|
||||
<strong>SQLite:</strong> sqlite:///email_server/server_data/smtp_server.db
|
||||
</button>
|
||||
<button type="button" class="list-group-item list-group-item-action" onclick="setDatabaseExample('mysql')">
|
||||
<strong>MySQL:</strong> mysql://user:password@localhost:3306/dbname
|
||||
</button>
|
||||
<button type="button" class="list-group-item list-group-item-action" onclick="setDatabaseExample('postgresql')">
|
||||
<strong>PostgreSQL:</strong> postgresql://user:password@localhost:5432/dbname
|
||||
</button>
|
||||
<button type="button" class="list-group-item list-group-item-action" onclick="setDatabaseExample('mssql')">
|
||||
<strong>MSSQL:</strong> mssql+pyodbc://user:password@server:1433/dbname?driver=ODBC+Driver+17+for+SQL+Server
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -126,36 +189,38 @@
|
||||
|
||||
<!-- Logging Settings -->
|
||||
<div class="card mb-4">
|
||||
<div class="card-header">
|
||||
<h5 class="mb-0">
|
||||
<i class="bi bi-journal-text me-2"></i>
|
||||
Logging Configuration
|
||||
<div class="card-header collapsed" data-bs-toggle="collapse" data-bs-target="#loggingSettings" aria-expanded="false">
|
||||
<h5 class="mb-0 d-flex justify-content-between align-items-center">
|
||||
<span><i class="bi bi-journal-text me-2"></i>Logging Configuration</span>
|
||||
<i class="bi bi-chevron-down"></i>
|
||||
</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="setting-section">
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Log Level</label>
|
||||
<div class="setting-description">Minimum log level to record</div>
|
||||
<select class="form-select" name="Logging.LOG_LEVEL">
|
||||
<option value="DEBUG" {{ 'selected' if settings['Logging']['LOG_LEVEL'] == 'DEBUG' else '' }}>DEBUG</option>
|
||||
<option value="INFO" {{ 'selected' if settings['Logging']['LOG_LEVEL'] == 'INFO' else '' }}>INFO</option>
|
||||
<option value="WARNING" {{ 'selected' if settings['Logging']['LOG_LEVEL'] == 'WARNING' else '' }}>WARNING</option>
|
||||
<option value="ERROR" {{ 'selected' if settings['Logging']['LOG_LEVEL'] == 'ERROR' else '' }}>ERROR</option>
|
||||
<option value="CRITICAL" {{ 'selected' if settings['Logging']['LOG_LEVEL'] == 'CRITICAL' else '' }}>CRITICAL</option>
|
||||
</select>
|
||||
<div id="loggingSettings" class="collapse">
|
||||
<div class="card-body">
|
||||
<div class="setting-section">
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Log Level</label>
|
||||
<div class="setting-description">Minimum log level to record</div>
|
||||
<select class="form-select" name="Logging.log_level">
|
||||
<option value="DEBUG" {{ 'selected' if settings['Logging']['log_level'] == 'DEBUG' else '' }}>DEBUG</option>
|
||||
<option value="INFO" {{ 'selected' if settings['Logging']['log_level'] == 'INFO' else '' }}>INFO</option>
|
||||
<option value="WARNING" {{ 'selected' if settings['Logging']['log_level'] == 'WARNING' else '' }}>WARNING</option>
|
||||
<option value="ERROR" {{ 'selected' if settings['Logging']['log_level'] == 'ERROR' else '' }}>ERROR</option>
|
||||
<option value="CRITICAL" {{ 'selected' if settings['Logging']['log_level'] == 'CRITICAL' else '' }}>CRITICAL</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Hide aiosmtpd INFO Messages</label>
|
||||
<div class="setting-description">Reduce verbose logging from aiosmtpd library</div>
|
||||
<select class="form-select" name="Logging.hide_info_aiosmtpd">
|
||||
<option value="true" {{ 'selected' if settings['Logging']['hide_info_aiosmtpd'] == 'true' else '' }}>Yes</option>
|
||||
<option value="false" {{ 'selected' if settings['Logging']['hide_info_aiosmtpd'] == 'false' else '' }}>No</option>
|
||||
</select>
|
||||
<div class="col-md-6">
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Hide aiosmtpd INFO Messages</label>
|
||||
<div class="setting-description">Reduce verbose logging from aiosmtpd library</div>
|
||||
<select class="form-select" name="Logging.hide_info_aiosmtpd">
|
||||
<option value="true" {{ 'selected' if settings['Logging']['hide_info_aiosmtpd'] == 'true' else '' }}>Yes</option>
|
||||
<option value="false" {{ 'selected' if settings['Logging']['hide_info_aiosmtpd'] == 'false' else '' }}>No</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -165,22 +230,24 @@
|
||||
|
||||
<!-- Relay Settings -->
|
||||
<div class="card mb-4">
|
||||
<div class="card-header">
|
||||
<h5 class="mb-0">
|
||||
<i class="bi bi-arrow-repeat me-2"></i>
|
||||
Email Relay Configuration
|
||||
<div class="card-header collapsed" data-bs-toggle="collapse" data-bs-target="#relaySettings" aria-expanded="false">
|
||||
<h5 class="mb-0 d-flex justify-content-between align-items-center">
|
||||
<span><i class="bi bi-arrow-repeat me-2"></i>Email Relay Configuration</span>
|
||||
<i class="bi bi-chevron-down"></i>
|
||||
</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="setting-section">
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Relay Timeout (seconds)</label>
|
||||
<div class="setting-description">Timeout for external SMTP connections when relaying emails</div>
|
||||
<input type="number"
|
||||
class="form-control"
|
||||
name="Relay.RELAY_TIMEOUT"
|
||||
value="{{ settings['Relay']['RELAY_TIMEOUT'] }}"
|
||||
min="5" max="300">
|
||||
<div id="relaySettings" class="collapse">
|
||||
<div class="card-body">
|
||||
<div class="setting-section">
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Relay Timeout (seconds)</label>
|
||||
<div class="setting-description">Timeout for external SMTP connections when relaying emails</div>
|
||||
<input type="number"
|
||||
class="form-control"
|
||||
name="Relay.relay_timeout"
|
||||
value="{{ settings['Relay']['relay_timeout'] }}"
|
||||
min="5" max="300">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -188,33 +255,57 @@
|
||||
|
||||
<!-- TLS Settings -->
|
||||
<div class="card mb-4">
|
||||
<div class="card-header">
|
||||
<h5 class="mb-0">
|
||||
<i class="bi bi-shield-lock me-2"></i>
|
||||
TLS/SSL Configuration
|
||||
<div class="card-header collapsed" data-bs-toggle="collapse" data-bs-target="#tlsSettings" aria-expanded="false">
|
||||
<h5 class="mb-0 d-flex justify-content-between align-items-center">
|
||||
<span><i class="bi bi-shield-lock me-2"></i>TLS/SSL Configuration</span>
|
||||
<i class="bi bi-chevron-down"></i>
|
||||
</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="setting-section">
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<div class="mb-3">
|
||||
<label class="form-label">TLS Certificate File</label>
|
||||
<div class="setting-description">Path to SSL certificate file (.crt or .pem)</div>
|
||||
<input type="text"
|
||||
class="form-control font-monospace"
|
||||
name="TLS.TLS_CERT_FILE"
|
||||
value="{{ settings['TLS']['TLS_CERT_FILE'] }}">
|
||||
<div id="tlsSettings" class="collapse">
|
||||
<div class="card-body">
|
||||
<div class="setting-section">
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<div class="mb-3">
|
||||
<label class="form-label">TLS Certificate File</label>
|
||||
<div class="setting-description">Path to SSL certificate file (.crt or .pem)</div>
|
||||
<div class="input-group">
|
||||
<input type="text"
|
||||
class="form-control font-monospace"
|
||||
name="TLS.tls_cert_file"
|
||||
value="{{ settings['TLS']['tls_cert_file'] }}">
|
||||
<input type="file"
|
||||
class="d-none"
|
||||
id="certFileUpload"
|
||||
accept=".crt,.pem">
|
||||
<button class="btn btn-outline-secondary"
|
||||
type="button"
|
||||
onclick="document.getElementById('certFileUpload').click()">
|
||||
<i class="bi bi-upload"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<div class="mb-3">
|
||||
<label class="form-label">TLS Private Key File</label>
|
||||
<div class="setting-description">Path to SSL private key file (.key or .pem)</div>
|
||||
<input type="text"
|
||||
class="form-control font-monospace"
|
||||
name="TLS.TLS_KEY_FILE"
|
||||
value="{{ settings['TLS']['TLS_KEY_FILE'] }}">
|
||||
<div class="col-md-6">
|
||||
<div class="mb-3">
|
||||
<label class="form-label">TLS Private Key File</label>
|
||||
<div class="setting-description">Path to SSL private key file (.key or .pem)</div>
|
||||
<div class="input-group">
|
||||
<input type="text"
|
||||
class="form-control font-monospace"
|
||||
name="TLS.tls_key_file"
|
||||
value="{{ settings['TLS']['tls_key_file'] }}">
|
||||
<input type="file"
|
||||
class="d-none"
|
||||
id="keyFileUpload"
|
||||
accept=".key,.pem">
|
||||
<button class="btn btn-outline-secondary"
|
||||
type="button"
|
||||
onclick="document.getElementById('keyFileUpload').click()">
|
||||
<i class="bi bi-upload"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -224,22 +315,39 @@
|
||||
|
||||
<!-- DKIM Settings -->
|
||||
<div class="card mb-4">
|
||||
<div class="card-header">
|
||||
<h5 class="mb-0">
|
||||
<i class="bi bi-key me-2"></i>
|
||||
DKIM Configuration
|
||||
<div class="card-header collapsed" data-bs-toggle="collapse" data-bs-target="#dkimSettings" aria-expanded="false">
|
||||
<h5 class="mb-0 d-flex justify-content-between align-items-center">
|
||||
<span><i class="bi bi-key me-2"></i>DKIM Configuration</span>
|
||||
<i class="bi bi-chevron-down"></i>
|
||||
</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="setting-section">
|
||||
<div class="mb-3">
|
||||
<label class="form-label">DKIM Key Size</label>
|
||||
<div class="setting-description">RSA key size for new DKIM keys (larger = more secure, slower)</div>
|
||||
<select class="form-select" name="DKIM.DKIM_KEY_SIZE">
|
||||
<option value="1024" {{ 'selected' if settings['DKIM']['DKIM_KEY_SIZE'] == '1024' else '' }}>1024 bits</option>
|
||||
<option value="2048" {{ 'selected' if settings['DKIM']['DKIM_KEY_SIZE'] == '2048' else '' }}>2048 bits (Recommended)</option>
|
||||
<option value="4096" {{ 'selected' if settings['DKIM']['DKIM_KEY_SIZE'] == '4096' else '' }}>4096 bits</option>
|
||||
</select>
|
||||
<div id="dkimSettings" class="collapse">
|
||||
<div class="card-body">
|
||||
<div class="setting-section">
|
||||
<div class="mb-3">
|
||||
<label class="form-label">DKIM Key Size</label>
|
||||
<div class="setting-description">RSA key size for new DKIM keys (larger = more secure, slower)</div>
|
||||
<select class="form-select" name="DKIM.dkim_key_size">
|
||||
<option value="1024" {{ 'selected' if settings['DKIM']['dkim_key_size'] == '1024' else '' }}>1024 bits</option>
|
||||
<option value="2048" {{ 'selected' if settings['DKIM']['dkim_key_size'] == '2048' else '' }}>2048 bits (Recommended)</option>
|
||||
<option value="4096" {{ 'selected' if settings['DKIM']['dkim_key_size'] == '4096' else '' }}>4096 bits</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="form-label">SPF Server IP</label>
|
||||
<div class="setting-description">Public IP address of server for SPF records (used if auto-detection fails)</div>
|
||||
<div class="input-group">
|
||||
<input type="text"
|
||||
class="form-control"
|
||||
name="DKIM.spf_server_ip"
|
||||
value="{{ settings['DKIM']['spf_server_ip'] }}"
|
||||
pattern="^(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$">
|
||||
<button class="btn btn-danger" type="button" onclick="getPublicIP()">
|
||||
<i class="bi bi-cloud-download me-1"></i>
|
||||
Get Public IP
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -334,7 +442,7 @@
|
||||
// Form validation
|
||||
document.querySelector('form').addEventListener('submit', function(e) {
|
||||
// Basic validation
|
||||
const ports = ['Server.SMTP_PORT', 'Server.SMTP_TLS_PORT'];
|
||||
const ports = ['Server.smtp_port', 'Server.smtp_tls_port'];
|
||||
for (const portField of ports) {
|
||||
const input = document.querySelector(`[name="${portField}"]`);
|
||||
const port = parseInt(input.value);
|
||||
@@ -347,8 +455,8 @@
|
||||
}
|
||||
|
||||
// Check if ports are different
|
||||
const smtpPort = document.querySelector('[name="Server.SMTP_PORT"]').value;
|
||||
const tlsPort = document.querySelector('[name="Server.SMTP_TLS_PORT"]').value;
|
||||
const smtpPort = document.querySelector('[name="Server.smtp_port"]').value;
|
||||
const tlsPort = document.querySelector('[name="Server.smtp_tls_port"]').value;
|
||||
if (smtpPort === tlsPort) {
|
||||
e.preventDefault();
|
||||
alert('SMTP and TLS ports must be different.');
|
||||
@@ -378,5 +486,121 @@
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// Handle certificate file uploads
|
||||
document.getElementById('certFileUpload').addEventListener('change', function(e) {
|
||||
const file = e.target.files[0];
|
||||
if (!file) return;
|
||||
|
||||
const formData = new FormData();
|
||||
formData.append('cert_file', file);
|
||||
|
||||
fetch('{{ url_for("email.upload_cert") }}', {
|
||||
method: 'POST',
|
||||
body: formData
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
if (data.status === 'success') {
|
||||
document.querySelector('[name="TLS.tls_cert_file"]').value = data.filepath;
|
||||
showToast('Certificate file uploaded successfully', 'success');
|
||||
} else {
|
||||
showToast(data.message || 'Failed to upload certificate file', 'danger');
|
||||
}
|
||||
})
|
||||
.catch(() => showToast('Failed to upload certificate file', 'danger'));
|
||||
});
|
||||
|
||||
document.getElementById('keyFileUpload').addEventListener('change', function(e) {
|
||||
const file = e.target.files[0];
|
||||
if (!file) return;
|
||||
|
||||
const formData = new FormData();
|
||||
formData.append('key_file', file);
|
||||
|
||||
fetch('{{ url_for("email.upload_key") }}', {
|
||||
method: 'POST',
|
||||
body: formData
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
if (data.status === 'success') {
|
||||
document.querySelector('[name="TLS.tls_key_file"]').value = data.filepath;
|
||||
showToast('Key file uploaded successfully', 'success');
|
||||
} else {
|
||||
showToast(data.message || 'Failed to upload key file', 'danger');
|
||||
}
|
||||
})
|
||||
.catch(() => showToast('Failed to upload key file', 'danger'));
|
||||
});
|
||||
|
||||
// Handle database examples
|
||||
function setDatabaseExample(type) {
|
||||
const urlInput = document.getElementById('databaseUrl');
|
||||
switch(type) {
|
||||
case 'sqlite':
|
||||
urlInput.value = 'sqlite:///email_server/server_data/smtp_server.db';
|
||||
break;
|
||||
case 'mysql':
|
||||
urlInput.value = 'mysql://user:password@localhost:3306/dbname';
|
||||
break;
|
||||
case 'postgresql':
|
||||
urlInput.value = 'postgresql://user:password@localhost:5432/dbname';
|
||||
break;
|
||||
case 'mssql':
|
||||
urlInput.value = 'mssql+pyodbc://user:password@server:1433/dbname?driver=ODBC+Driver+17+for+SQL+Server';
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Test database connection
|
||||
function testDatabaseConnection() {
|
||||
const url = document.getElementById('databaseUrl').value;
|
||||
fetch('{{ url_for("email.test_database_connection_endpoint") }}', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Accept': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({ url: url })
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
showToast(data.message || (data.status === 'success' ? 'Database connection successful!' : 'Failed to connect to database'),
|
||||
data.status === 'success' ? 'success' : 'danger');
|
||||
})
|
||||
.catch(() => showToast('Failed to test database connection', 'danger'));
|
||||
}
|
||||
|
||||
// Get public IP
|
||||
function getPublicIP() {
|
||||
fetch('{{ url_for("email.get_server_ip") }}')
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
if (data.ip) {
|
||||
document.querySelector('[name="DKIM.spf_server_ip"]').value = data.ip;
|
||||
showToast('Public IP fetched successfully', 'success');
|
||||
} else {
|
||||
showToast('Failed to fetch public IP', 'danger');
|
||||
}
|
||||
})
|
||||
.catch(() => showToast('Failed to fetch public IP', 'danger'));
|
||||
}
|
||||
|
||||
// Handle form submission
|
||||
document.getElementById('settingsForm').addEventListener('submit', function(e) {
|
||||
// Handle empty server banner
|
||||
const serverBanner = document.querySelector('[name="Server.server_banner"]');
|
||||
if (serverBanner && !serverBanner.value.trim()) {
|
||||
serverBanner.value = '""';
|
||||
}
|
||||
|
||||
// Log form data being submitted
|
||||
const formData = new FormData(this);
|
||||
console.log('Submitting settings with data:');
|
||||
for (let [key, value] of formData.entries()) {
|
||||
console.log(`${key}: ${value}`);
|
||||
}
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
||||
|
||||
@@ -40,8 +40,8 @@
|
||||
</li>
|
||||
|
||||
<li class="nav-item mb-1">
|
||||
<a href="{{ url_for('email.users_list') }}"
|
||||
class="nav-link text-white {{ 'active' if request.endpoint in ['email.users_list', 'email.add_user'] else '' }}">
|
||||
<a href="{{ url_for('email.senders_list') }}"
|
||||
class="nav-link text-white {{ 'active' if request.endpoint in ['email.senders_list', 'email.add_user'] else '' }}">
|
||||
<i class="bi bi-people me-2"></i>
|
||||
Allowed Senders
|
||||
<span class="badge bg-secondary ms-auto">{{ user_count if user_count is defined else '' }}</span>
|
||||
|
||||
Reference in New Issue
Block a user