first commit
This commit is contained in:
@@ -0,0 +1,260 @@
|
||||
/* Custom CSS for SMTP Management Frontend */
|
||||
|
||||
/* Enhanced dark theme tweaks */
|
||||
.card {
|
||||
border: 1px solid #404040;
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
|
||||
.card-header {
|
||||
border-bottom: 1px solid #404040;
|
||||
}
|
||||
|
||||
.table-dark {
|
||||
--bs-table-bg: #2d3748;
|
||||
--bs-table-striped-bg: #374151;
|
||||
}
|
||||
|
||||
/* Status badges */
|
||||
.status-active {
|
||||
background-color: #10b981 !important;
|
||||
}
|
||||
|
||||
.status-inactive {
|
||||
background-color: #ef4444 !important;
|
||||
}
|
||||
|
||||
.status-pending {
|
||||
background-color: #f59e0b !important;
|
||||
}
|
||||
|
||||
/* Custom form styling */
|
||||
.form-control:focus {
|
||||
border-color: #3b82f6;
|
||||
box-shadow: 0 0 0 0.2rem rgba(59, 130, 246, 0.25);
|
||||
}
|
||||
|
||||
.form-select:focus {
|
||||
border-color: #3b82f6;
|
||||
box-shadow: 0 0 0 0.2rem rgba(59, 130, 246, 0.25);
|
||||
}
|
||||
|
||||
/* Copy button styling */
|
||||
.copy-btn {
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.copy-btn::after {
|
||||
content: "Copied!";
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
background-color: #10b981;
|
||||
color: white;
|
||||
padding: 2px 8px;
|
||||
border-radius: 4px;
|
||||
font-size: 12px;
|
||||
opacity: 0;
|
||||
transition: opacity 0.3s ease;
|
||||
}
|
||||
|
||||
.copy-btn.copied::after {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
/* DNS record styling */
|
||||
.dns-record {
|
||||
background-color: #1f2937;
|
||||
border: 1px solid #374151;
|
||||
border-radius: 4px;
|
||||
font-family: 'Courier New', monospace;
|
||||
font-size: 14px;
|
||||
padding: 12px;
|
||||
margin: 8px 0;
|
||||
}
|
||||
|
||||
.dns-record-header {
|
||||
color: #9ca3af;
|
||||
font-weight: bold;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.dns-record-value {
|
||||
color: #e5e7eb;
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
/* Log entry styling */
|
||||
.log-entry {
|
||||
border-left: 4px solid #374151;
|
||||
padding-left: 12px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.log-entry.log-error {
|
||||
border-left-color: #ef4444;
|
||||
}
|
||||
|
||||
.log-entry.log-warning {
|
||||
border-left-color: #f59e0b;
|
||||
}
|
||||
|
||||
.log-entry.log-info {
|
||||
border-left-color: #3b82f6;
|
||||
}
|
||||
|
||||
.log-entry.log-success {
|
||||
border-left-color: #10b981;
|
||||
}
|
||||
|
||||
/* Statistics cards */
|
||||
.stat-card {
|
||||
background: linear-gradient(135deg, #1f2937 0%, #374151 100%);
|
||||
border: 1px solid #4b5563;
|
||||
border-radius: 8px;
|
||||
padding: 20px;
|
||||
text-align: center;
|
||||
transition: transform 0.2s ease, box-shadow 0.2s ease;
|
||||
}
|
||||
|
||||
.stat-card:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.4);
|
||||
}
|
||||
|
||||
.stat-number {
|
||||
font-size: 2.5rem;
|
||||
font-weight: bold;
|
||||
color: #3b82f6;
|
||||
}
|
||||
|
||||
.stat-label {
|
||||
color: #9ca3af;
|
||||
font-size: 0.9rem;
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
/* Loading states */
|
||||
.loading {
|
||||
opacity: 0.6;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.spinner-border-sm {
|
||||
width: 1rem;
|
||||
height: 1rem;
|
||||
}
|
||||
|
||||
/* Responsive table wrapper */
|
||||
.table-responsive {
|
||||
border-radius: 8px;
|
||||
border: 1px solid #404040;
|
||||
}
|
||||
|
||||
/* Alert styling */
|
||||
.alert {
|
||||
border: none;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.alert-success {
|
||||
background-color: rgba(16, 185, 129, 0.1);
|
||||
color: #10b981;
|
||||
border-left: 4px solid #10b981;
|
||||
}
|
||||
|
||||
.alert-danger {
|
||||
background-color: rgba(239, 68, 68, 0.1);
|
||||
color: #ef4444;
|
||||
border-left: 4px solid #ef4444;
|
||||
}
|
||||
|
||||
.alert-warning {
|
||||
background-color: rgba(245, 158, 11, 0.1);
|
||||
color: #f59e0b;
|
||||
border-left: 4px solid #f59e0b;
|
||||
}
|
||||
|
||||
.alert-info {
|
||||
background-color: rgba(59, 130, 246, 0.1);
|
||||
color: #3b82f6;
|
||||
border-left: 4px solid #3b82f6;
|
||||
}
|
||||
|
||||
/* Custom scrollbar */
|
||||
.custom-scrollbar {
|
||||
scrollbar-width: thin;
|
||||
scrollbar-color: #4b5563 #1f2937;
|
||||
}
|
||||
|
||||
.custom-scrollbar::-webkit-scrollbar {
|
||||
width: 8px;
|
||||
}
|
||||
|
||||
.custom-scrollbar::-webkit-scrollbar-track {
|
||||
background: #1f2937;
|
||||
}
|
||||
|
||||
.custom-scrollbar::-webkit-scrollbar-thumb {
|
||||
background-color: #4b5563;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.custom-scrollbar::-webkit-scrollbar-thumb:hover {
|
||||
background-color: #6b7280;
|
||||
}
|
||||
|
||||
/* Animation classes */
|
||||
.fade-in {
|
||||
animation: fadeIn 0.3s ease-in;
|
||||
}
|
||||
|
||||
@keyframes fadeIn {
|
||||
from { opacity: 0; transform: translateY(10px); }
|
||||
to { opacity: 1; transform: translateY(0); }
|
||||
}
|
||||
|
||||
.slide-in {
|
||||
animation: slideIn 0.3s ease-out;
|
||||
}
|
||||
|
||||
@keyframes slideIn {
|
||||
from { transform: translateX(-20px); opacity: 0; }
|
||||
to { transform: translateX(0); opacity: 1; }
|
||||
}
|
||||
|
||||
/* Tooltip styling */
|
||||
.tooltip {
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.tooltip-inner {
|
||||
background-color: #1f2937;
|
||||
border: 1px solid #374151;
|
||||
}
|
||||
|
||||
.tooltip.bs-tooltip-top .tooltip-arrow::before {
|
||||
border-top-color: #374151;
|
||||
}
|
||||
|
||||
.tooltip.bs-tooltip-bottom .tooltip-arrow::before {
|
||||
border-bottom-color: #374151;
|
||||
}
|
||||
|
||||
/* Mobile responsiveness */
|
||||
@media (max-width: 768px) {
|
||||
.stat-number {
|
||||
font-size: 1.8rem;
|
||||
}
|
||||
|
||||
.dns-record {
|
||||
font-size: 12px;
|
||||
padding: 8px;
|
||||
}
|
||||
|
||||
.table-responsive {
|
||||
font-size: 14px;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,266 @@
|
||||
// DKIM Management functionality
|
||||
const DKIMManagement = {
|
||||
// Check DNS records for a domain
|
||||
checkDomainDNS: async function(domain, selector, checkDkimUrl, checkSpfUrl) {
|
||||
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(checkDkimUrl, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/x-www-form-urlencoded'
|
||||
},
|
||||
body: new URLSearchParams({
|
||||
domain: domain,
|
||||
selector: selector
|
||||
})
|
||||
});
|
||||
const dkimResult = await dkimResponse.json();
|
||||
|
||||
// Check SPF DNS
|
||||
const spfResponse = await fetch(checkSpfUrl, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/x-www-form-urlencoded'
|
||||
},
|
||||
body: new URLSearchParams({
|
||||
domain: domain
|
||||
})
|
||||
});
|
||||
const spfResult = await spfResponse.json();
|
||||
|
||||
// Get DKIM key status from the card class
|
||||
const domainCard = document.getElementById(`domain-${domain.replace('.', '-')}`);
|
||||
const isActive = domainCard && domainCard.classList.contains('dkim-active');
|
||||
|
||||
// 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" style="background-color: #6c757d;"></span><small class="text-muted">Disabled</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
|
||||
this.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>';
|
||||
}
|
||||
},
|
||||
|
||||
// Show DNS check results in modal
|
||||
showDNSResults: function(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>
|
||||
|
||||
<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>
|
||||
<div class="records-container mt-2">
|
||||
${dkimRecordsHtml}
|
||||
</div>
|
||||
` : ''}
|
||||
</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>
|
||||
${spfRecordHtml}
|
||||
` : ''}
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
document.getElementById('dnsResults').innerHTML = resultsHtml;
|
||||
new bootstrap.Modal(document.getElementById('dnsResultModal')).show();
|
||||
},
|
||||
|
||||
// Check all domains' DNS records
|
||||
checkAllDNS: async function(checkDkimUrl, checkSpfUrl) {
|
||||
const domains = document.querySelectorAll('[id^="domain-"]');
|
||||
const results = [];
|
||||
|
||||
// Show a progress indicator
|
||||
showToast('Checking DNS records for all domains...', 'info');
|
||||
|
||||
for (const domainCard of domains) {
|
||||
try {
|
||||
const domainId = domainCard.id.split('-')[1];
|
||||
// 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
|
||||
const selectorElement = domainCard.querySelector('code');
|
||||
|
||||
if (selectorElement) {
|
||||
const selector = selectorElement.textContent;
|
||||
|
||||
// Check DKIM DNS
|
||||
const dkimResponse = await fetch(checkDkimUrl, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/x-www-form-urlencoded',
|
||||
},
|
||||
body: `domain=${encodeURIComponent(domainName)}&selector=${encodeURIComponent(selector)}`
|
||||
});
|
||||
const dkimResult = await dkimResponse.json();
|
||||
|
||||
// Check SPF DNS
|
||||
const spfResponse = await fetch(checkSpfUrl, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/x-www-form-urlencoded',
|
||||
},
|
||||
body: `domain=${encodeURIComponent(domainName)}`
|
||||
});
|
||||
const spfResult = await spfResponse.json();
|
||||
|
||||
results.push({
|
||||
domain: domainName,
|
||||
dkim: dkimResult,
|
||||
spf: spfResult
|
||||
});
|
||||
|
||||
// Update individual status indicators
|
||||
const dkimStatus = document.getElementById(`dkim-status-${domainName.replace('.', '-')}`);
|
||||
const spfStatus = document.getElementById(`spf-status-${domainName.replace('.', '-')}`);
|
||||
|
||||
if (dkimStatus) {
|
||||
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>';
|
||||
}
|
||||
}
|
||||
|
||||
if (spfStatus) {
|
||||
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>';
|
||||
}
|
||||
}
|
||||
|
||||
// Small delay between checks to avoid overwhelming the DNS server
|
||||
await new Promise(resolve => setTimeout(resolve, 300));
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error checking DNS for domain:', error);
|
||||
}
|
||||
}
|
||||
|
||||
// Show combined results in modal
|
||||
this.showAllDNSResults(results);
|
||||
},
|
||||
|
||||
// Show combined DNS check results
|
||||
showAllDNSResults: function(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>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
document.getElementById('dnsResults').innerHTML = resultsHtml;
|
||||
new bootstrap.Modal(document.getElementById('dnsResultModal')).show();
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,285 @@
|
||||
/* Custom JavaScript for SMTP Management Frontend */
|
||||
|
||||
// Global utilities
|
||||
const SMTPManagement = {
|
||||
// Copy text to clipboard
|
||||
copyToClipboard: function(text, button) {
|
||||
navigator.clipboard.writeText(text).then(() => {
|
||||
this.showCopySuccess(button);
|
||||
}).catch(err => {
|
||||
console.error('Failed to copy: ', err);
|
||||
this.showCopyError(button);
|
||||
});
|
||||
},
|
||||
|
||||
// Show copy success feedback
|
||||
showCopySuccess: function(button) {
|
||||
const originalText = button.innerHTML;
|
||||
button.innerHTML = '<i class="fas fa-check me-1"></i>Copied!';
|
||||
button.classList.remove('btn-outline-light');
|
||||
button.classList.add('btn-success');
|
||||
|
||||
setTimeout(() => {
|
||||
button.innerHTML = originalText;
|
||||
button.classList.remove('btn-success');
|
||||
button.classList.add('btn-outline-light');
|
||||
}, 2000);
|
||||
},
|
||||
|
||||
// Show copy error feedback
|
||||
showCopyError: function(button) {
|
||||
const originalText = button.innerHTML;
|
||||
button.innerHTML = '<i class="fas fa-times me-1"></i>Failed!';
|
||||
button.classList.remove('btn-outline-light');
|
||||
button.classList.add('btn-danger');
|
||||
|
||||
setTimeout(() => {
|
||||
button.innerHTML = originalText;
|
||||
button.classList.remove('btn-danger');
|
||||
button.classList.add('btn-outline-light');
|
||||
}, 2000);
|
||||
},
|
||||
|
||||
// Format timestamps
|
||||
formatTimestamp: function(timestamp) {
|
||||
const date = new Date(timestamp);
|
||||
return date.toLocaleString();
|
||||
},
|
||||
|
||||
// Validate email address
|
||||
validateEmail: function(email) {
|
||||
const re = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
||||
return re.test(email);
|
||||
},
|
||||
|
||||
// Validate IP address
|
||||
validateIP: function(ip) {
|
||||
const ipv4Regex = /^(?:(?: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]?)$/;
|
||||
const ipv6Regex = /^(?:[0-9a-fA-F]{1,4}:){7}[0-9a-fA-F]{1,4}$/;
|
||||
return ipv4Regex.test(ip) || ipv6Regex.test(ip);
|
||||
},
|
||||
|
||||
// Show loading state
|
||||
showLoading: function(element) {
|
||||
element.classList.add('loading');
|
||||
const spinner = element.querySelector('.spinner-border');
|
||||
if (spinner) {
|
||||
spinner.style.display = 'inline-block';
|
||||
}
|
||||
},
|
||||
|
||||
// Hide loading state
|
||||
hideLoading: function(element) {
|
||||
element.classList.remove('loading');
|
||||
const spinner = element.querySelector('.spinner-border');
|
||||
if (spinner) {
|
||||
spinner.style.display = 'none';
|
||||
}
|
||||
},
|
||||
|
||||
// Show toast notification
|
||||
showToast: function(message, type = 'info') {
|
||||
const toastContainer = document.getElementById('toast-container') || this.createToastContainer();
|
||||
const toast = this.createToast(message, type);
|
||||
toastContainer.appendChild(toast);
|
||||
|
||||
// Auto-remove after 5 seconds
|
||||
setTimeout(() => {
|
||||
toast.remove();
|
||||
}, 5000);
|
||||
},
|
||||
|
||||
// Create toast container
|
||||
createToastContainer: function() {
|
||||
const container = document.createElement('div');
|
||||
container.id = 'toast-container';
|
||||
container.className = 'position-fixed top-0 end-0 p-3';
|
||||
container.style.zIndex = '1056';
|
||||
document.body.appendChild(container);
|
||||
return container;
|
||||
},
|
||||
|
||||
// Create toast element
|
||||
createToast: function(message, type) {
|
||||
const toast = document.createElement('div');
|
||||
toast.className = `toast align-items-center text-white bg-${type} border-0`;
|
||||
toast.setAttribute('role', 'alert');
|
||||
toast.innerHTML = `
|
||||
<div class="d-flex">
|
||||
<div class="toast-body">${message}</div>
|
||||
<button type="button" class="btn-close btn-close-white me-2 m-auto" data-bs-dismiss="toast"></button>
|
||||
</div>
|
||||
`;
|
||||
|
||||
// Initialize Bootstrap toast
|
||||
const bsToast = new bootstrap.Toast(toast);
|
||||
bsToast.show();
|
||||
|
||||
return toast;
|
||||
},
|
||||
|
||||
// Auto-refresh functionality
|
||||
autoRefresh: function(url, interval = 30000) {
|
||||
setInterval(() => {
|
||||
fetch(url, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'X-Requested-With': 'XMLHttpRequest'
|
||||
}
|
||||
})
|
||||
.then(response => response.text())
|
||||
.then(html => {
|
||||
const parser = new DOMParser();
|
||||
const doc = parser.parseFromString(html, 'text/html');
|
||||
const newContent = doc.querySelector('#refresh-content');
|
||||
const currentContent = document.querySelector('#refresh-content');
|
||||
|
||||
if (newContent && currentContent) {
|
||||
currentContent.innerHTML = newContent.innerHTML;
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Auto-refresh failed:', error);
|
||||
});
|
||||
}, interval);
|
||||
}
|
||||
};
|
||||
|
||||
// DNS verification functionality
|
||||
const DNSVerification = {
|
||||
// Check DNS record
|
||||
checkDNSRecord: function(domain, recordType, expectedValue) {
|
||||
return fetch('/email/check-dns', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'X-Requested-With': 'XMLHttpRequest'
|
||||
},
|
||||
body: JSON.stringify({
|
||||
domain: domain,
|
||||
record_type: recordType,
|
||||
expected_value: expectedValue
|
||||
})
|
||||
})
|
||||
.then(response => response.json());
|
||||
},
|
||||
|
||||
// Update DNS status indicator
|
||||
updateDNSStatus: function(element, status, message) {
|
||||
const statusIcon = element.querySelector('.dns-status-icon');
|
||||
const statusText = element.querySelector('.dns-status-text');
|
||||
|
||||
if (statusIcon && statusText) {
|
||||
statusIcon.className = `dns-status-icon fas ${status === 'valid' ? 'fa-check-circle text-success' : 'fa-times-circle text-danger'}`;
|
||||
statusText.textContent = message;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Form validation
|
||||
const FormValidation = {
|
||||
// Real-time email validation
|
||||
validateEmailField: function(input) {
|
||||
const isValid = SMTPManagement.validateEmail(input.value);
|
||||
this.updateFieldStatus(input, isValid, 'Please enter a valid email address');
|
||||
return isValid;
|
||||
},
|
||||
|
||||
// Real-time IP validation
|
||||
validateIPField: function(input) {
|
||||
const isValid = SMTPManagement.validateIP(input.value);
|
||||
this.updateFieldStatus(input, isValid, 'Please enter a valid IP address');
|
||||
return isValid;
|
||||
},
|
||||
|
||||
// Update field validation status
|
||||
updateFieldStatus: function(input, isValid, errorMessage) {
|
||||
const feedback = input.parentNode.querySelector('.invalid-feedback');
|
||||
|
||||
if (isValid) {
|
||||
input.classList.remove('is-invalid');
|
||||
input.classList.add('is-valid');
|
||||
if (feedback) feedback.textContent = '';
|
||||
} else {
|
||||
input.classList.remove('is-valid');
|
||||
input.classList.add('is-invalid');
|
||||
if (feedback) feedback.textContent = errorMessage;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Initialize on DOM load
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
// Initialize tooltips
|
||||
const tooltipTriggerList = [].slice.call(document.querySelectorAll('[data-bs-toggle="tooltip"]'));
|
||||
tooltipTriggerList.map(function(tooltipTriggerEl) {
|
||||
return new bootstrap.Tooltip(tooltipTriggerEl);
|
||||
});
|
||||
|
||||
// Initialize form validation
|
||||
const emailInputs = document.querySelectorAll('input[type="email"]');
|
||||
emailInputs.forEach(input => {
|
||||
input.addEventListener('blur', () => FormValidation.validateEmailField(input));
|
||||
});
|
||||
|
||||
const ipInputs = document.querySelectorAll('input[data-validate="ip"]');
|
||||
ipInputs.forEach(input => {
|
||||
input.addEventListener('blur', () => FormValidation.validateIPField(input));
|
||||
});
|
||||
|
||||
// Initialize auto-refresh for logs page
|
||||
if (document.querySelector('#logs-page')) {
|
||||
SMTPManagement.autoRefresh(window.location.href, 30000);
|
||||
}
|
||||
|
||||
// Initialize current IP detection
|
||||
const currentIPSpan = document.querySelector('#current-ip');
|
||||
if (currentIPSpan) {
|
||||
fetch('https://api.ipify.org?format=json')
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
currentIPSpan.textContent = data.ip;
|
||||
})
|
||||
.catch(() => {
|
||||
currentIPSpan.textContent = 'Unable to detect';
|
||||
});
|
||||
}
|
||||
|
||||
// Initialize copy buttons
|
||||
const copyButtons = document.querySelectorAll('.copy-btn');
|
||||
copyButtons.forEach(button => {
|
||||
button.addEventListener('click', function() {
|
||||
const textToCopy = this.getAttribute('data-copy') || this.nextElementSibling.textContent;
|
||||
SMTPManagement.copyToClipboard(textToCopy, this);
|
||||
});
|
||||
});
|
||||
|
||||
// Initialize DNS check buttons
|
||||
const dnsCheckButtons = document.querySelectorAll('.dns-check-btn');
|
||||
dnsCheckButtons.forEach(button => {
|
||||
button.addEventListener('click', function() {
|
||||
const domain = this.getAttribute('data-domain');
|
||||
const recordType = this.getAttribute('data-record-type');
|
||||
const expectedValue = this.getAttribute('data-expected-value');
|
||||
const statusElement = this.closest('.dns-record').querySelector('.dns-status');
|
||||
|
||||
SMTPManagement.showLoading(this);
|
||||
|
||||
DNSVerification.checkDNSRecord(domain, recordType, expectedValue)
|
||||
.then(result => {
|
||||
DNSVerification.updateDNSStatus(statusElement, result.status, result.message);
|
||||
SMTPManagement.hideLoading(this);
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('DNS check failed:', error);
|
||||
DNSVerification.updateDNSStatus(statusElement, 'error', 'DNS check failed');
|
||||
SMTPManagement.hideLoading(this);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
// Export for use in other scripts
|
||||
window.SMTPManagement = SMTPManagement;
|
||||
window.DNSVerification = DNSVerification;
|
||||
window.FormValidation = FormValidation;
|
||||
Reference in New Issue
Block a user