web_UI working - needs more tweaking

This commit is contained in:
nahakubuilde
2025-06-07 11:57:21 +01:00
parent 7053b82d30
commit ce0f7e0ac9
38 changed files with 1257 additions and 2799 deletions

View File

@@ -0,0 +1,112 @@
{% extends "base.html" %}
{% block title %}Add Domain - Email Server Management{% endblock %}
{% block page_title %}Add New Domain{% endblock %}
{% block content %}
<div class="row justify-content-center">
<div class="col-lg-6">
<div class="card">
<div class="card-header">
<h5 class="mb-0">
<i class="bi bi-plus-circle me-2"></i>
Add New Domain
</h5>
</div>
<div class="card-body">
<form method="post">
<div class="mb-3">
<label for="domain_name" class="form-label">
<i class="bi bi-globe me-1"></i>
Domain Name
</label>
<input type="text"
class="form-control"
id="domain_name"
name="domain_name"
placeholder="example.com"
required
pattern="^[a-zA-Z0-9][a-zA-Z0-9-]*[a-zA-Z0-9]*\.?[a-zA-Z]{2,}$">
<div class="form-text">
Enter the domain name that will be used for sending emails (e.g., example.com)
</div>
</div>
<div class="alert alert-info">
<h6 class="alert-heading">
<i class="bi bi-info-circle me-2"></i>
What happens next?
</h6>
<ul class="mb-0">
<li>Domain will be added to the system</li>
<li>DKIM key pair will be automatically generated</li>
<li>You'll need to configure DNS records</li>
<li>Add users or whitelist IPs for authentication</li>
</ul>
</div>
<div class="d-flex justify-content-between">
<a href="{{ url_for('email.domains_list') }}" class="btn btn-secondary">
<i class="bi bi-arrow-left me-2"></i>
Back to Domains
</a>
<button type="submit" class="btn btn-primary">
<i class="bi bi-plus-circle me-2"></i>
Add Domain
</button>
</div>
</form>
</div>
</div>
<div class="card mt-4">
<div class="card-header">
<h6 class="mb-0">
<i class="bi bi-question-circle me-2"></i>
Domain Requirements
</h6>
</div>
<div class="card-body">
<div class="row">
<div class="col-md-6">
<h6 class="text-success">
<i class="bi bi-check-circle me-1"></i>
Valid Examples
</h6>
<ul class="list-unstyled">
<li><code>example.com</code></li>
<li><code>mail.example.com</code></li>
<li><code>my-domain.org</code></li>
<li><code>company.co.uk</code></li>
</ul>
</div>
<div class="col-md-6">
<h6 class="text-danger">
<i class="bi bi-x-circle me-1"></i>
Invalid Examples
</h6>
<ul class="list-unstyled">
<li><code>http://example.com</code></li>
<li><code>example</code></li>
<li><code>.example.com</code></li>
<li><code>example..com</code></li>
</ul>
</div>
</div>
</div>
</div>
</div>
</div>
{% endblock %}
{% block extra_js %}
<script>
document.getElementById('domain_name').addEventListener('input', function(e) {
// Convert to lowercase and remove protocol if present
let value = e.target.value.toLowerCase();
value = value.replace(/^https?:\/\//, '');
value = value.replace(/\/$/, '');
e.target.value = value;
});
</script>
{% endblock %}

View File

@@ -0,0 +1,228 @@
{% extends "base.html" %}
{% block title %}Add IP Address - Email Server{% endblock %}
{% block content %}
<div class="container-fluid">
<div class="row">
<div class="col-md-8 mx-auto">
<div class="card">
<div class="card-header">
<h4 class="mb-0">
<i class="bi bi-shield-plus me-2"></i>
Add IP Address to Whitelist
</h4>
</div>
<div class="card-body">
<form method="POST">
<div class="mb-3">
<label for="ip_address" class="form-label">IP Address</label>
<input type="text"
class="form-control font-monospace"
id="ip_address"
name="ip_address"
required
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]?)$"
placeholder="192.168.1.100"
value="{{ request.args.get('ip', '') }}">
<div class="form-text">
IPv4 address that will be allowed to send emails without authentication
</div>
</div>
<div class="mb-4">
<label for="domain_id" class="form-label">Authorized Domain</label>
<select class="form-select" id="domain_id" name="domain_id" required>
<option value="">Select a domain...</option>
{% for domain in domains %}
<option value="{{ domain.id }}">{{ domain.domain_name }}</option>
{% endfor %}
</select>
<div class="form-text">
This IP will only be able to send emails for the selected domain
</div>
</div>
<div class="alert alert-warning">
<h6 class="alert-heading">
<i class="bi bi-exclamation-triangle me-2"></i>
Security Note
</h6>
<ul class="mb-0">
<li>Only whitelist trusted IP addresses</li>
<li>This IP can send emails without username/password authentication</li>
<li>The IP is restricted to the selected domain only</li>
<li>Use static IP addresses for reliable access</li>
</ul>
</div>
<div class="d-flex justify-content-between">
<a href="{{ url_for('email.ips_list') }}" class="btn btn-secondary">
<i class="bi bi-arrow-left me-2"></i>
Back to IP List
</a>
<button type="submit" class="btn btn-success">
<i class="bi bi-shield-plus me-2"></i>
Add to Whitelist
</button>
</div>
</form>
</div>
</div>
</div>
</div>
</div>
<!-- Current IP Detection and Available Domains -->
<div class="row mt-4">
<div class="col-md-4 mx-auto">
<div class="card">
<div class="card-header">
<h6 class="mb-0">
<i class="bi bi-geo-alt me-2"></i>
Your Current IP
</h6>
</div>
<div class="card-body text-center">
<div class="fw-bold font-monospace fs-5 mb-2" id="current-ip">
<span class="spinner-border spinner-border-sm me-2"></span>
Detecting...
</div>
<button type="button" class="btn btn-outline-primary btn-sm" onclick="useCurrentIP()">
<i class="bi bi-arrow-up me-1"></i>
Use This IP
</button>
</div>
</div>
</div>
{% if domains %}
<div class="col-md-4 mx-auto">
<div class="card">
<div class="card-header">
<h6 class="mb-0">
<i class="bi bi-server me-2"></i>
Available Domains
</h6>
</div>
<div class="card-body">
{% for domain in domains %}
<div class="mb-2">
<div class="fw-bold">{{ domain.domain_name }}</div>
<small class="text-muted">
Created: {{ domain.created_at.strftime('%Y-%m-%d') }}
</small>
</div>
{% if not loop.last %}<hr class="my-2">{% endif %}
{% endfor %}
</div>
</div>
</div>
{% endif %}
</div>
<!-- Example Use Cases -->
<div class="row mt-4">
<div class="col-md-8 mx-auto">
<div class="card">
<div class="card-header">
<h6 class="mb-0">
<i class="bi bi-lightbulb me-2"></i>
Common Use Cases
</h6>
</div>
<div class="card-body">
<div class="row">
<div class="col-md-6">
<h6 class="text-primary">
<i class="bi bi-server me-1"></i>
Application Servers
</h6>
<p class="text-muted small">
Web applications that need to send transactional emails
(password resets, notifications, etc.)
</p>
</div>
<div class="col-md-6">
<h6 class="text-success">
<i class="bi bi-clock me-1"></i>
Scheduled Tasks
</h6>
<p class="text-muted small">
Cron jobs or scheduled scripts that send automated
reports or alerts
</p>
</div>
</div>
<div class="row">
<div class="col-md-6">
<h6 class="text-warning">
<i class="bi bi-monitor me-1"></i>
Monitoring Systems
</h6>
<p class="text-muted small">
Monitoring tools that send alerts and status updates
to administrators
</p>
</div>
<div class="col-md-6">
<h6 class="text-info">
<i class="bi bi-cloud me-1"></i>
Cloud Services
</h6>
<p class="text-muted small">
Cloud-based applications or services that need to
send emails on behalf of your domain
</p>
</div>
</div>
</div>
</div>
</div>
</div>
{% endblock %}
{% block extra_js %}
<script>
// Detect current IP address
async function detectCurrentIP() {
try {
const response = await fetch('https://api.ipify.org?format=json');
const data = await response.json();
document.getElementById('current-ip').innerHTML =
`<span class="text-primary">${data.ip}</span>`;
} catch (error) {
document.getElementById('current-ip').innerHTML =
'<span class="text-muted">Unable to detect</span>';
}
}
function useCurrentIP() {
const currentIPElement = document.getElementById('current-ip');
const ip = currentIPElement.textContent.trim();
if (ip && ip !== 'Detecting...' && ip !== 'Unable to detect') {
document.getElementById('ip_address').value = ip;
// Focus on domain selection
document.getElementById('domain_id').focus();
} else {
alert('Unable to detect current IP address');
}
}
// IP address validation
document.getElementById('ip_address').addEventListener('input', function(e) {
const ip = e.target.value;
const ipPattern = /^(?:(?: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]?)$/;
if (ip && !ipPattern.test(ip)) {
e.target.setCustomValidity('Please enter a valid IPv4 address');
} else {
e.target.setCustomValidity('');
}
});
// Auto-detect IP on page load
detectCurrentIP();
</script>
{% endblock %}

View File

@@ -0,0 +1,176 @@
{% extends "base.html" %}
{% block title %}Add User - Email Server{% endblock %}
{% block content %}
<div class="container-fluid">
<div class="row">
<div class="col-md-8 mx-auto">
<div class="card">
<div class="card-header">
<h4 class="mb-0">
<i class="bi bi-person-plus me-2"></i>
Add New User
</h4>
</div>
<div class="card-body">
<form method="POST">
<div class="mb-3">
<label for="email" class="form-label">Email Address</label>
<input type="email"
class="form-control"
id="email"
name="email"
required
placeholder="user@example.com">
<div class="form-text">
The email address for authentication and sending
</div>
</div>
<div class="mb-3">
<label for="password" class="form-label">Password</label>
<input type="password"
class="form-control"
id="password"
name="password"
required
minlength="6">
<div class="form-text">
Minimum 6 characters
</div>
</div>
<div class="mb-3">
<label for="domain_id" class="form-label">Domain</label>
<select class="form-select" id="domain_id" name="domain_id" required>
<option value="">Select a domain...</option>
{% for domain in domains %}
<option value="{{ domain.id }}">{{ domain.domain_name }}</option>
{% endfor %}
</select>
<div class="form-text">
The domain this user belongs to
</div>
</div>
<div class="mb-4">
<div class="form-check">
<input class="form-check-input"
type="checkbox"
id="can_send_as_domain"
name="can_send_as_domain">
<label class="form-check-label" for="can_send_as_domain">
<strong>Domain Administrator</strong>
</label>
<div class="form-text">
If checked, user can send emails as any address in their domain.
Otherwise, user can only send as their own email address.
</div>
</div>
</div>
<div class="alert alert-info">
<h6 class="alert-heading">
<i class="bi bi-info-circle me-2"></i>
Permission Levels
</h6>
<ul class="mb-0">
<li><strong>Regular User:</strong> Can only send emails from their own email address</li>
<li><strong>Domain Admin:</strong> Can send emails from any address in their domain (e.g., noreply@domain.com, support@domain.com)</li>
</ul>
</div>
<div class="d-flex justify-content-between">
<a href="{{ url_for('email.users_list') }}" class="btn btn-secondary">
<i class="bi bi-arrow-left me-2"></i>
Back to Users
</a>
<button type="submit" class="btn btn-success">
<i class="bi bi-person-plus me-2"></i>
Add User
</button>
</div>
</form>
</div>
</div>
</div>
</div>
</div>
<!-- Domain information sidebar -->
<div class="row mt-4">
{% if domains %}
<div class="col-md-6 mx-auto">
<div class="card">
<div class="card-header">
<h6 class="mb-0">
<i class="bi bi-info-circle me-2"></i>
Available Domains
</h6>
</div>
<div class="card-body">
<div class="row">
{% for domain in domains %}
<div class="col-md-6 mb-2">
<div class="border rounded p-2">
<div class="fw-bold">{{ domain.domain_name }}</div>
<small class="text-muted">
Created: {{ domain.created_at.strftime('%Y-%m-%d') }}
</small>
</div>
</div>
{% endfor %}
</div>
</div>
</div>
</div>
{% endif %}
</div>
{% endblock %}
{% block extra_js %}
<script>
// Auto-fill domain based on email input
document.getElementById('email').addEventListener('input', function(e) {
const email = e.target.value;
const atIndex = email.indexOf('@');
if (atIndex > -1) {
const domain = email.substring(atIndex + 1).toLowerCase();
const domainSelect = document.getElementById('domain_id');
// Try to find matching domain in select options
for (let option of domainSelect.options) {
if (option.text.toLowerCase() === domain) {
domainSelect.value = option.value;
break;
}
}
}
});
// Show/hide domain admin explanation
document.getElementById('can_send_as_domain').addEventListener('change', function(e) {
const isChecked = e.target.checked;
const domainSelect = document.getElementById('domain_id');
const selectedDomain = domainSelect.options[domainSelect.selectedIndex]?.text || 'domain.com';
// Update help text dynamically
const helpText = e.target.closest('.form-check').querySelector('.form-text');
if (isChecked) {
helpText.innerHTML = `User can send as any address in ${selectedDomain} (e.g., noreply@${selectedDomain}, support@${selectedDomain})`;
} else {
helpText.innerHTML = 'User can only send as their own email address.';
}
});
// Update help text when domain changes
document.getElementById('domain_id').addEventListener('change', function(e) {
const checkbox = document.getElementById('can_send_as_domain');
if (checkbox.checked) {
checkbox.dispatchEvent(new Event('change'));
}
});
</script>
{% endblock %}

View File

@@ -0,0 +1,227 @@
<!DOCTYPE html>
<html lang="en" data-bs-theme="dark">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{% block title %}Email Server Management{% endblock %}</title>
<!-- Bootstrap CSS -->
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css" rel="stylesheet">
<!-- Bootstrap Icons -->
<link href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.0/font/bootstrap-icons.css" rel="stylesheet">
<!-- Custom CSS -->
<style>
:root {
--sidebar-width: 280px;
}
body {
background-color: #1a1a1a;
color: #e0e0e0;
}
.main-container {
display: flex;
min-height: 100vh;
}
.content-area {
flex: 1;
margin-left: var(--sidebar-width);
padding: 20px;
transition: margin-left 0.3s ease;
}
.navbar-brand {
color: #fff !important;
}
.card {
background-color: #2d2d2d;
border: 1px solid #404040;
}
.table-dark {
--bs-table-bg: #2d2d2d;
--bs-table-border-color: #404040;
}
.btn-outline-light:hover {
background-color: #495057;
}
.alert-success {
background-color: #0f5132;
border-color: #146c43;
color: #75b798;
}
.alert-danger {
background-color: #58151c;
border-color: #842029;
color: #ea868f;
}
.alert-warning {
background-color: #664d03;
border-color: #997404;
color: #ffda6a;
}
.alert-info {
background-color: #055160;
border-color: #087990;
color: #6edff6;
}
.form-control:focus {
border-color: #0d6efd;
box-shadow: 0 0 0 0.25rem rgba(13, 110, 253, 0.25);
}
.form-select:focus {
border-color: #0d6efd;
box-shadow: 0 0 0 0.25rem rgba(13, 110, 253, 0.25);
}
.text-muted {
color: #adb5bd !important;
}
.border-success {
border-color: #198754 !important;
}
.border-danger {
border-color: #dc3545 !important;
}
.text-success {
color: #75b798 !important;
}
.text-danger {
color: #ea868f !important;
}
.text-warning {
color: #ffda6a !important;
}
/* Custom scrollbar */
::-webkit-scrollbar {
width: 8px;
}
::-webkit-scrollbar-track {
background: #2d2d2d;
}
::-webkit-scrollbar-thumb {
background: #495057;
border-radius: 4px;
}
::-webkit-scrollbar-thumb:hover {
background: #6c757d;
}
</style>
<!-- Custom SMTP Management CSS -->
<link href="{{ url_for('email.static', filename='css/smtp-management.css') }}" rel="stylesheet">
{% block extra_css %}{% endblock %}
</head>
<body>
<div class="main-container">
<!-- Sidebar -->
{% include 'sidebar_email.html' %}
<!-- Main content -->
<div class="content-area">
<!-- Top navbar -->
<nav class="navbar navbar-expand-lg navbar-dark bg-dark mb-4">
<div class="container-fluid">
<span class="navbar-brand mb-0 h1">
<i class="bi bi-envelope-fill me-2"></i>
{% block page_title %}Email Server Management{% endblock %}
</span>
<div class="navbar-nav ms-auto">
<span class="navbar-text">
<i class="bi bi-clock-fill me-1"></i>
<span id="current-time"></span>
</span>
</div>
</div>
</nav>
<!-- Flash messages -->
{% with messages = get_flashed_messages(with_categories=true) %}
{% if messages %}
<div class="row">
<div class="col-12">
{% for category, message in messages %}
<div class="alert alert-{{ 'danger' if category == 'error' else category }} alert-dismissible fade show" role="alert">
<i class="bi bi-{{ 'exclamation-triangle' if category == 'error' else 'check-circle' if category == 'success' else 'info-circle' }} me-2"></i>
{{ message }}
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
</div>
{% endfor %}
</div>
</div>
{% endif %}
{% endwith %}
<!-- Page content -->
<main>
{% block content %}{% endblock %}
</main>
</div>
</div>
<!-- Bootstrap JS -->
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/js/bootstrap.bundle.min.js"></script>
<!-- Custom JS -->
<script>
// Update current time
function updateTime() {
const now = new Date();
const timeString = now.toLocaleTimeString();
const dateString = now.toLocaleDateString();
document.getElementById('current-time').textContent = `${dateString} ${timeString}`;
}
// Update time every second
setInterval(updateTime, 1000);
updateTime(); // Initial call
// Auto-dismiss alerts after 5 seconds
setTimeout(function() {
const alerts = document.querySelectorAll('.alert:not(.alert-permanent)');
alerts.forEach(function(alert) {
const bootstrapAlert = new bootstrap.Alert(alert);
bootstrapAlert.close();
});
}, 5000);
// Confirmation dialogs for delete actions
document.addEventListener('DOMContentLoaded', function() {
const deleteButtons = document.querySelectorAll('[data-confirm]');
deleteButtons.forEach(function(button) {
button.addEventListener('click', function(e) {
if (!confirm(this.getAttribute('data-confirm'))) {
e.preventDefault();
}
});
});
});
</script>
<!-- Custom SMTP Management JavaScript -->
<script src="{{ url_for('email.static', filename='js/smtp-management.js') }}"></script>
{% block extra_js %}{% endblock %}
</body>
</html>

View File

@@ -0,0 +1,285 @@
{% extends "base.html" %}
{% block title %}Dashboard - Email Server Management{% endblock %}
{% block page_title %}Dashboard{% endblock %}
{% block content %}
<div class="row">
<!-- Statistics Cards -->
<div class="col-lg-3 col-md-6 mb-4">
<div class="card border-primary">
<div class="card-body">
<div class="d-flex align-items-center">
<div class="flex-grow-1">
<h5 class="card-title text-primary mb-1">
<i class="bi bi-globe me-2"></i>
Domains
</h5>
<h3 class="mb-0">{{ domain_count }}</h3>
<small class="text-muted">Active domains</small>
</div>
<div class="fs-2 text-primary opacity-50">
<i class="bi bi-globe"></i>
</div>
</div>
</div>
</div>
</div>
<div class="col-lg-3 col-md-6 mb-4">
<div class="card border-success">
<div class="card-body">
<div class="d-flex align-items-center">
<div class="flex-grow-1">
<h5 class="card-title text-success mb-1">
<i class="bi bi-people me-2"></i>
Users
</h5>
<h3 class="mb-0">{{ user_count }}</h3>
<small class="text-muted">Authenticated users</small>
</div>
<div class="fs-2 text-success opacity-50">
<i class="bi bi-people"></i>
</div>
</div>
</div>
</div>
</div>
<div class="col-lg-3 col-md-6 mb-4">
<div class="card border-warning">
<div class="card-body">
<div class="d-flex align-items-center">
<div class="flex-grow-1">
<h5 class="card-title text-warning mb-1">
<i class="bi bi-shield-check me-2"></i>
DKIM Keys
</h5>
<h3 class="mb-0">{{ dkim_count }}</h3>
<small class="text-muted">Active DKIM keys</small>
</div>
<div class="fs-2 text-warning opacity-50">
<i class="bi bi-shield-check"></i>
</div>
</div>
</div>
</div>
</div>
<div class="col-lg-3 col-md-6 mb-4">
<div class="card border-info">
<div class="card-body">
<div class="d-flex align-items-center">
<div class="flex-grow-1">
<h5 class="card-title text-info mb-1">
<i class="bi bi-activity me-2"></i>
Status
</h5>
<h6 class="text-success mb-0">
<i class="bi bi-circle-fill me-1" style="font-size: 0.5rem;"></i>
Online
</h6>
<small class="text-muted">Server running</small>
</div>
<div class="fs-2 text-info opacity-50">
<i class="bi bi-activity"></i>
</div>
</div>
</div>
</div>
</div>
</div>
<div class="row">
<!-- Recent Email Activity -->
<div class="col-lg-8 mb-4">
<div class="card">
<div class="card-header d-flex justify-content-between align-items-center">
<h5 class="mb-0">
<i class="bi bi-envelope me-2"></i>
Recent Email Activity
</h5>
<a href="{{ url_for('email.logs', type='emails') }}" class="btn btn-outline-light btn-sm">
View All
</a>
</div>
<div class="card-body p-0">
{% if recent_emails %}
<div class="table-responsive">
<table class="table table-dark table-hover mb-0">
<thead>
<tr>
<th>Time</th>
<th>From</th>
<th>To</th>
<th>Status</th>
<th>DKIM</th>
</tr>
</thead>
<tbody>
{% for email in recent_emails %}
<tr>
<td>
<small class="text-muted">
{{ email.created_at.strftime('%H:%M:%S') }}
</small>
</td>
<td>
<span class="text-truncate d-inline-block" style="max-width: 150px;" title="{{ email.mail_from }}">
{{ email.mail_from }}
</span>
</td>
<td>
<span class="text-truncate d-inline-block" style="max-width: 150px;" title="{{ email.to_address }}">
{{ email.to_address }}
</span>
</td>
<td>
{% if email.status == 'relayed' %}
<span class="badge bg-success">
<i class="bi bi-check-circle me-1"></i>
Sent
</span>
{% else %}
<span class="badge bg-danger">
<i class="bi bi-x-circle me-1"></i>
Failed
</span>
{% endif %}
</td>
<td>
{% if email.dkim_signed %}
<span class="text-success">
<i class="bi bi-shield-check" title="DKIM Signed"></i>
</span>
{% else %}
<span class="text-muted">
<i class="bi bi-shield-x" title="Not DKIM Signed"></i>
</span>
{% endif %}
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
{% else %}
<div class="text-center py-4">
<i class="bi bi-envelope text-muted fs-1"></i>
<p class="text-muted mt-2">No email activity yet</p>
</div>
{% endif %}
</div>
</div>
</div>
<!-- Recent Authentication Activity -->
<div class="col-lg-4 mb-4">
<div class="card">
<div class="card-header d-flex justify-content-between align-items-center">
<h5 class="mb-0">
<i class="bi bi-shield-lock me-2"></i>
Recent Auth Activity
</h5>
<a href="{{ url_for('email.logs', type='auth') }}" class="btn btn-outline-light btn-sm">
View All
</a>
</div>
<div class="card-body p-0">
{% if recent_auths %}
<div class="list-group list-group-flush">
{% for auth in recent_auths %}
<div class="list-group-item list-group-item-dark d-flex justify-content-between align-items-start">
<div class="ms-2 me-auto">
<div class="fw-bold">
{% if auth.success %}
<i class="bi bi-check-circle text-success me-1"></i>
{% else %}
<i class="bi bi-x-circle text-danger me-1"></i>
{% endif %}
{{ auth.auth_type|title }}
</div>
<small class="text-muted">
{{ auth.identifier }}
</small>
<br>
<small class="text-muted">
{{ auth.created_at.strftime('%H:%M:%S') }}
</small>
</div>
<small class="text-muted">
{{ auth.ip_address }}
</small>
</div>
{% endfor %}
</div>
{% else %}
<div class="text-center py-4">
<i class="bi bi-shield-lock text-muted fs-1"></i>
<p class="text-muted mt-2">No authentication activity yet</p>
</div>
{% endif %}
</div>
</div>
</div>
</div>
<!-- Quick Actions -->
<div class="row">
<div class="col-12">
<div class="card">
<div class="card-header">
<h5 class="mb-0">
<i class="bi bi-lightning me-2"></i>
Quick Actions
</h5>
</div>
<div class="card-body">
<div class="row">
<div class="col-md-3 mb-3">
<div class="d-grid">
<a href="{{ url_for('email.add_domain') }}" class="btn btn-outline-primary">
<i class="bi bi-plus-circle me-2"></i>
Add Domain
</a>
</div>
</div>
<div class="col-md-3 mb-3">
<div class="d-grid">
<a href="{{ url_for('email.add_user') }}" class="btn btn-outline-success">
<i class="bi bi-person-plus me-2"></i>
Add User
</a>
</div>
</div>
<div class="col-md-3 mb-3">
<div class="d-grid">
<a href="{{ url_for('email.add_ip') }}" class="btn btn-outline-warning">
<i class="bi bi-shield-plus me-2"></i>
Whitelist IP
</a>
</div>
</div>
<div class="col-md-3 mb-3">
<div class="d-grid">
<a href="{{ url_for('email.settings') }}" class="btn btn-outline-info">
<i class="bi bi-gear me-2"></i>
Settings
</a>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
{% endblock %}
{% block extra_js %}
<script>
// Auto-refresh dashboard every 30 seconds
setTimeout(function() {
location.reload();
}, 30000);
</script>
{% endblock %}

View File

@@ -0,0 +1,377 @@
{% extends "base.html" %}
{% block title %}DKIM Keys - Email Server{% endblock %}
{% block extra_css %}
<style>
.dns-record {
font-family: 'Courier New', monospace;
color: black;
background-color: var(--bs-gray-100);
border-radius: 0.375rem;
padding: 0.75rem;
border: 1px solid var(--bs-border-color);
word-break: break-all;
}
.status-indicator {
width: 12px;
height: 12px;
border-radius: 50%;
display: inline-block;
margin-right: 0.5rem;
}
.status-success { background-color: #28a745; }
.status-warning { background-color: #ffc107; }
.status-danger { background-color: #dc3545; }
</style>
{% 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 }}">
<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 }}">
<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>
<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>
<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>
<div class="ms-auto">
<i class="bi bi-chevron-down" id="chevron-{{ item.domain.id }}"></i>
</div>
</div>
</div>
<div class="collapse" id="collapse-{{ item.domain.id }}">
<div class="card-body">
<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>
</div>
</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];
// 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;
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);
});
}
// 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);
}
});
});
});
</script>
{% endblock %}

View File

@@ -0,0 +1,194 @@
{% extends "base.html" %}
{% block title %}Domains - Email Server Management{% endblock %}
{% block page_title %}Domain Management{% endblock %}
{% block content %}
<div class="d-flex justify-content-between align-items-center mb-4">
<h2>
<i class="bi bi-globe me-2"></i>
Domains
</h2>
<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 class="card">
<div class="card-header">
<h5 class="mb-0">
<i class="bi bi-list-ul me-2"></i>
All Domains
</h5>
</div>
<div class="card-body p-0">
{% if domains %}
<div class="table-responsive">
<table class="table table-dark table-hover mb-0">
<thead>
<tr>
<th>Domain Name</th>
<th>Status</th>
<th>Created</th>
<th>Users</th>
<th>DKIM</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
{% for domain in domains %}
<tr>
<td>
<div class="fw-bold">{{ domain.domain_name }}</div>
</td>
<td>
{% if domain.is_active %}
<span class="badge bg-success">
<i class="bi bi-check-circle me-1"></i>
Active
</span>
{% else %}
<span class="badge bg-danger">
<i class="bi bi-x-circle me-1"></i>
Inactive
</span>
{% endif %}
</td>
<td>
<small class="text-muted">
{{ domain.created_at.strftime('%Y-%m-%d %H:%M') }}
</small>
</td>
<td>
<span class="badge bg-info">
{{ domain.users|length if domain.users else 0 }} users
</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>
</span>
{% else %}
<span class="text-warning">
<i class="bi bi-shield-exclamation" title="No DKIM Key"></i>
</span>
{% endif %}
</td>
<td>
<div class="btn-group btn-group-sm" role="group">
<!-- Edit Button -->
<a href="{{ url_for('email.edit_domain', domain_id=domain.id) }}"
class="btn btn-outline-primary"
title="Edit Domain">
<i class="bi bi-pencil"></i>
</a>
<!-- Toggle Enable/Disable Button -->
<form method="post" action="{{ url_for('email.toggle_domain', domain_id=domain.id) }}" class="d-inline">
{% if domain.is_active %}
<button type="submit"
class="btn btn-outline-warning"
onclick="return confirm('Are you sure you want to disable domain \'{{ domain.domain_name }}\'?')"
title="Disable Domain">
<i class="bi bi-pause-circle"></i>
</button>
{% else %}
<button type="submit"
class="btn btn-outline-success"
onclick="return confirm('Are you sure you want to enable domain \'{{ domain.domain_name }}\'?')"
title="Enable Domain">
<i class="bi bi-play-circle"></i>
</button>
{% endif %}
</form>
<!-- Remove Button -->
<form method="post" action="{{ url_for('email.remove_domain', domain_id=domain.id) }}" class="d-inline">
<button type="submit"
class="btn btn-outline-danger"
onclick="return confirm('WARNING: This will permanently delete domain \'{{ domain.domain_name }}\' and ALL associated data (users, IPs, DKIM keys). This action cannot be undone. Are you sure you want to continue?')"
title="Permanently Remove Domain">
<i class="bi bi-trash"></i>
</button>
</form>
</div>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
{% else %}
<div class="text-center py-5">
<i class="bi bi-globe text-muted" style="font-size: 4rem;"></i>
<h4 class="text-muted mt-3">No domains configured</h4>
<p class="text-muted">Get started by adding your first domain</p>
<a href="{{ url_for('email.add_domain') }}" class="btn btn-primary">
<i class="bi bi-plus-circle me-2"></i>
Add Your First Domain
</a>
</div>
{% endif %}
</div>
</div>
{% if domains %}
<div class="row mt-4">
<div class="col-md-6">
<div class="card">
<div class="card-header">
<h6 class="mb-0">
<i class="bi bi-info-circle me-2"></i>
Domain Information
</h6>
</div>
<div class="card-body">
<ul class="list-unstyled mb-0">
<li class="mb-2">
<i class="bi bi-check-circle text-success me-2"></i>
<strong>Active domains:</strong> {{ domains|selectattr('is_active')|list|length }}
</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' }}
</li>
</ul>
</div>
</div>
</div>
<div class="col-md-6">
<div class="card">
<div class="card-header">
<h6 class="mb-0">
<i class="bi bi-lightbulb me-2"></i>
Quick Tips
</h6>
</div>
<div class="card-body">
<ul class="list-unstyled mb-0">
<li class="mb-2">
<i class="bi bi-arrow-right text-primary me-2"></i>
DKIM keys are automatically generated for new domains
</li>
<li class="mb-2">
<i class="bi bi-arrow-right text-primary me-2"></i>
Configure DNS records after adding domains
</li>
<li>
<i class="bi bi-arrow-right text-primary me-2"></i>
Add users or whitelist IPs for authentication
</li>
</ul>
</div>
</div>
</div>
</div>
{% endif %}
{% endblock %}

View File

@@ -0,0 +1,129 @@
{% extends "base.html" %}
{% block title %}Edit DKIM Key - Email Server{% 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>
Edit DKIM Key for {{ domain.domain_name }}
</h2>
<a href="{{ url_for('email.dkim_list') }}" class="btn btn-outline-secondary">
<i class="bi bi-arrow-left me-2"></i>
Back to DKIM Keys
</a>
</div>
<div class="row justify-content-center">
<div class="col-lg-8">
<div class="card">
<div class="card-header">
<h5 class="mb-0">
<i class="bi bi-key me-2"></i>
DKIM Key Configuration
</h5>
</div>
<div class="card-body">
<form method="post">
<div class="mb-3">
<label for="domain_name" class="form-label">Domain</label>
<input type="text"
class="form-control"
id="domain_name"
value="{{ domain.domain_name }}"
readonly>
<div class="form-text">The domain this DKIM key belongs to (read-only)</div>
</div>
<div class="mb-3">
<label for="selector" class="form-label">DKIM Selector</label>
<input type="text"
class="form-control"
id="selector"
name="selector"
value="{{ dkim_key.selector }}"
maxlength="50"
pattern="[a-zA-Z0-9_-]+"
required>
<div class="form-text">
The DKIM selector (alphanumeric, hyphens, and underscores only).
This will be used in DNS record names like <code>[selector]._domainkey.{{ domain.domain_name }}</code>
</div>
</div>
<div class="mb-3">
<label class="form-label">Key Information</label>
<div class="row">
<div class="col-md-6">
<strong>Created:</strong><br>
<small class="text-muted">{{ dkim_key.created_at.strftime('%Y-%m-%d %H:%M:%S') }}</small>
</div>
<div class="col-md-6">
<strong>Status:</strong><br>
{% if dkim_key.is_active %}
<span class="badge bg-success">Active</span>
{% else %}
<span class="badge bg-secondary">Inactive</span>
{% endif %}
</div>
</div>
</div>
<div class="alert alert-warning">
<i class="bi bi-exclamation-triangle me-2"></i>
<strong>Important:</strong> Changing the selector will require updating your DNS records.
The new DNS record name will be <code id="dns-preview">{{ dkim_key.selector }}._domainkey.{{ domain.domain_name }}</code>
</div>
<div class="d-flex justify-content-between">
<a href="{{ url_for('email.dkim_list') }}" class="btn btn-secondary">
<i class="bi bi-x-circle me-2"></i>
Cancel
</a>
<button type="submit" class="btn btn-primary">
<i class="bi bi-check-circle me-2"></i>
Update DKIM Selector
</button>
</div>
</form>
</div>
</div>
</div>
</div>
</div>
{% endblock %}
{% block extra_js %}
<script>
// Update DNS preview when selector changes
document.getElementById('selector').addEventListener('input', function() {
const selector = this.value || 'default';
const domain = '{{ domain.domain_name }}';
document.getElementById('dns-preview').textContent = `${selector}._domainkey.${domain}`;
});
// Form validation
document.querySelector('form').addEventListener('submit', function(e) {
const selector = document.getElementById('selector').value.trim();
if (!selector) {
e.preventDefault();
alert('Selector is required');
return;
}
if (!/^[a-zA-Z0-9_-]+$/.test(selector)) {
e.preventDefault();
alert('Selector must contain only letters, numbers, hyphens, and underscores');
return;
}
if (selector.length > 50) {
e.preventDefault();
alert('Selector must be 50 characters or less');
return;
}
});
</script>
{% endblock %}

View File

@@ -0,0 +1,164 @@
{% extends "base.html" %}
{% block title %}Edit Domain{% endblock %}
{% block navbar %}
{% include 'sidebar_email.html' %}
{% endblock %}
{% block content %}
<div class="container-fluid">
<div class="row">
<div class="col-md-8 offset-md-2">
<div class="card">
<div class="card-header bg-primary text-white d-flex justify-content-between align-items-center">
<h4 class="mb-0">
<i class="fas fa-edit me-2"></i>Edit Domain
</h4>
<a href="{{ url_for('email.domains') }}" class="btn btn-light btn-sm">
<i class="fas fa-arrow-left me-1"></i>Back to Domains
</a>
</div>
<div class="card-body">
<form method="POST" class="needs-validation" novalidate>
<div class="mb-3">
<label for="domain_name" class="form-label">
<i class="fas fa-globe me-1"></i>Domain Name
</label>
<input type="text"
class="form-control"
id="domain_name"
name="domain_name"
value="{{ domain.domain_name }}"
placeholder="example.com"
pattern="^[a-zA-Z0-9]([a-zA-Z0-9\-]{0,61}[a-zA-Z0-9])?(\.[a-zA-Z0-9]([a-zA-Z0-9\-]{0,61}[a-zA-Z0-9])?)*$"
required>
<div class="invalid-feedback">
Please provide a valid domain name.
</div>
<div class="form-text">
<i class="fas fa-info-circle me-1"></i>
Enter a fully qualified domain name (e.g., example.com)
</div>
</div>
<div class="row">
<div class="col-md-6">
<div class="alert alert-info">
<h6><i class="fas fa-info-circle me-1"></i>Current Status</h6>
<p class="mb-1">
<strong>Status:</strong>
{% if domain.is_active %}
<span class="badge bg-success">Active</span>
{% else %}
<span class="badge bg-danger">Inactive</span>
{% endif %}
</p>
<p class="mb-1">
<strong>Created:</strong> {{ domain.created_at.strftime('%Y-%m-%d %H:%M:%S') }}
</p>
{% if domain.updated_at %}
<p class="mb-0">
<strong>Last Updated:</strong> {{ domain.updated_at.strftime('%Y-%m-%d %H:%M:%S') }}
</p>
{% endif %}
</div>
</div>
<div class="col-md-6">
<div class="alert alert-warning">
<h6><i class="fas fa-exclamation-triangle me-1"></i>Note</h6>
<p class="mb-0">
Changing the domain name will affect all associated users,
IP addresses, and DKIM keys. Make sure to update your DNS
records accordingly.
</p>
</div>
</div>
</div>
<div class="d-flex justify-content-between">
<a href="{{ url_for('email.domains') }}" class="btn btn-secondary">
<i class="fas fa-times me-1"></i>Cancel
</a>
<button type="submit" class="btn btn-primary">
<i class="fas fa-save me-1"></i>Update Domain
</button>
</div>
</form>
</div>
</div>
<!-- Associated Records Card -->
<div class="card mt-4">
<div class="card-header bg-info text-white">
<h5 class="mb-0">
<i class="fas fa-link me-2"></i>Associated Records
</h5>
</div>
<div class="card-body">
<div class="row">
<div class="col-md-4">
<div class="text-center">
<div class="h3 text-primary">{{ domain.users|length }}</div>
<div class="text-muted">
<i class="fas fa-users me-1"></i>Users
</div>
</div>
</div>
<div class="col-md-4">
<div class="text-center">
<div class="h3 text-success">{{ domain.authorized_ips|length }}</div>
<div class="text-muted">
<i class="fas fa-network-wired me-1"></i>IP Addresses
</div>
</div>
</div>
<div class="col-md-4">
<div class="text-center">
<div class="h3 text-warning">{{ domain.dkim_keys|length }}</div>
<div class="text-muted">
<i class="fas fa-key me-1"></i>DKIM Keys
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<script>
// Bootstrap validation
(function() {
'use strict';
window.addEventListener('load', function() {
var forms = document.getElementsByClassName('needs-validation');
var validation = Array.prototype.filter.call(forms, function(form) {
form.addEventListener('submit', function(event) {
if (form.checkValidity() === false) {
event.preventDefault();
event.stopPropagation();
}
form.classList.add('was-validated');
}, false);
});
}, false);
})();
// Domain name validation
document.getElementById('domain_name').addEventListener('input', function(e) {
const value = e.target.value.toLowerCase();
e.target.value = value;
// Basic domain validation
const domainRegex = /^[a-zA-Z0-9]([a-zA-Z0-9\-]{0,61}[a-zA-Z0-9])?(\.[a-zA-Z0-9]([a-zA-Z0-9\-]{0,61}[a-zA-Z0-9])?)*$/;
if (value && !domainRegex.test(value)) {
e.target.setCustomValidity('Invalid domain format');
} else {
e.target.setCustomValidity('');
}
});
</script>
{% endblock %}

View File

@@ -0,0 +1,163 @@
{% extends "base.html" %}
{% block title %}Edit IP Whitelist - SMTP Management{% endblock %}
{% block content %}
<div class="row">
<div class="col-md-8">
<div class="card">
<div class="card-header">
<h5 class="mb-0">
<i class="bi bi-pencil-square me-2"></i>
Edit IP Whitelist Entry
</h5>
</div>
<div class="card-body">
<form method="POST">
<div class="mb-3">
<label for="ip_address" class="form-label">IP Address</label>
<input type="text"
class="form-control"
id="ip_address"
name="ip_address"
value="{{ ip_record.ip_address }}"
placeholder="e.g., 192.168.1.1 or 192.168.1.0/24"
required>
<div class="form-text">
Enter a single IP address or CIDR block
</div>
</div>
<div class="mb-3">
<label for="domain_id" class="form-label">Domain</label>
<select class="form-select" id="domain_id" name="domain_id" required>
<option value="">Select a domain</option>
{% for domain in domains %}
<option value="{{ domain.id }}"
{% if domain.id == ip_record.domain_id %}selected{% endif %}>
{{ domain.domain_name }}
</option>
{% endfor %}
</select>
<div class="form-text">
This IP will be able to send emails for the selected domain
</div>
</div>
<div class="d-flex gap-2">
<button type="submit" class="btn btn-primary">
<i class="bi bi-check-lg me-1"></i>
Update IP Whitelist
</button>
<a href="{{ url_for('email.ips_list') }}" class="btn btn-secondary">
<i class="bi bi-x-lg me-1"></i>
Cancel
</a>
</div>
</form>
</div>
</div>
</div>
<div class="col-md-4">
<div class="card">
<div class="card-header">
<h6 class="mb-0">
<i class="bi bi-info-circle me-2"></i>
Current Configuration
</h6>
</div>
<div class="card-body">
<dl class="row mb-0">
<dt class="col-sm-4">Current IP:</dt>
<dd class="col-sm-8">
<code>{{ ip_record.ip_address }}</code>
</dd>
<dt class="col-sm-4">Domain:</dt>
<dd class="col-sm-8">
{% for domain in domains %}
{% if domain.id == ip_record.domain_id %}
<span class="badge bg-secondary">{{ domain.domain_name }}</span>
{% endif %}
{% endfor %}
</dd>
<dt class="col-sm-4">Status:</dt>
<dd class="col-sm-8">
{% if ip_record.is_active %}
<span class="badge bg-success">
<i class="bi bi-check-circle me-1"></i>
Active
</span>
{% else %}
<span class="badge bg-danger">
<i class="bi bi-x-circle me-1"></i>
Inactive
</span>
{% endif %}
</dd>
<dt class="col-sm-4">Created:</dt>
<dd class="col-sm-8">
<small class="text-muted">
{{ ip_record.created_at.strftime('%Y-%m-%d %H:%M') }}
</small>
</dd>
</dl>
</div>
</div>
<div class="card mt-3">
<div class="card-header">
<h6 class="mb-0">
<i class="bi bi-shield-check me-2"></i>
IP Format Examples
</h6>
</div>
<div class="card-body">
<ul class="list-unstyled mb-0 small">
<li class="mb-2">
<strong>Single IP:</strong><br>
<code>192.168.1.100</code>
</li>
<li class="mb-2">
<strong>Subnet (CIDR):</strong><br>
<code>192.168.1.0/24</code>
</li>
<li>
<strong>Localhost:</strong><br>
<code>127.0.0.1</code>
</li>
</ul>
</div>
</div>
</div>
</div>
{% endblock %}
{% block extra_js %}
<script>
document.addEventListener('DOMContentLoaded', function() {
// Auto-focus on IP address field
document.getElementById('ip_address').focus();
// Add IP validation
const ipInput = document.getElementById('ip_address');
ipInput.addEventListener('blur', function() {
const ip = this.value.trim();
if (ip && !isValidIP(ip)) {
this.classList.add('is-invalid');
} else {
this.classList.remove('is-invalid');
}
});
function isValidIP(ip) {
// Basic IP validation (IPv4 with optional CIDR)
const ipRegex = /^(\d{1,3}\.){3}\d{1,3}(\/\d{1,2})?$/;
if (!ipRegex.test(ip)) return false;
const parts = ip.split('/')[0].split('.');
return parts.every(part => parseInt(part) >= 0 && parseInt(part) <= 255);
}
});
</script>
{% endblock %}

View File

@@ -0,0 +1,190 @@
{% extends "base.html" %}
{% block title %}Edit User - SMTP Management{% endblock %}
{% block content %}
<div class="row">
<div class="col-md-8">
<div class="card">
<div class="card-header">
<h5 class="mb-0">
<i class="bi bi-person-fill-gear me-2"></i>
Edit User
</h5>
</div>
<div class="card-body">
<form method="POST">
<div class="mb-3">
<label for="email" class="form-label">Email Address</label>
<input type="email"
class="form-control"
id="email"
name="email"
value="{{ user.email }}"
required>
</div>
<div class="mb-3">
<label for="password" class="form-label">Password</label>
<input type="password"
class="form-control"
id="password"
name="password"
placeholder="Leave blank to keep current password">
<div class="form-text">
Only enter a password if you want to change it
</div>
</div>
<div class="mb-3">
<label for="domain_id" class="form-label">Domain</label>
<select class="form-select" id="domain_id" name="domain_id" required>
<option value="">Select a domain</option>
{% for domain in domains %}
<option value="{{ domain.id }}"
{% if domain.id == user.domain_id %}selected{% endif %}>
{{ domain.domain_name }}
</option>
{% endfor %}
</select>
</div>
<div class="mb-3">
<div class="form-check">
<input class="form-check-input"
type="checkbox"
id="can_send_as_domain"
name="can_send_as_domain"
{% if user.can_send_as_domain %}checked{% endif %}>
<label class="form-check-label" for="can_send_as_domain">
<strong>Can send as domain</strong>
</label>
<div class="form-text">
Allow this user to send emails using any address within their domain
</div>
</div>
</div>
<div class="d-flex gap-2">
<button type="submit" class="btn btn-primary">
<i class="bi bi-check-lg me-1"></i>
Update User
</button>
<a href="{{ url_for('email.users_list') }}" class="btn btn-secondary">
<i class="bi bi-x-lg me-1"></i>
Cancel
</a>
</div>
</form>
</div>
</div>
</div>
<div class="col-md-4">
<div class="card">
<div class="card-header">
<h6 class="mb-0">
<i class="bi bi-info-circle me-2"></i>
Current User Details
</h6>
</div>
<div class="card-body">
<dl class="row mb-0">
<dt class="col-sm-4">Email:</dt>
<dd class="col-sm-8">
<code>{{ user.email }}</code>
</dd>
<dt class="col-sm-4">Domain:</dt>
<dd class="col-sm-8">
{% for domain in domains %}
{% if domain.id == user.domain_id %}
<span class="badge bg-secondary">{{ domain.domain_name }}</span>
{% endif %}
{% endfor %}
</dd>
<dt class="col-sm-4">Status:</dt>
<dd class="col-sm-8">
{% if user.is_active %}
<span class="badge bg-success">
<i class="bi bi-check-circle me-1"></i>
Active
</span>
{% else %}
<span class="badge bg-danger">
<i class="bi bi-x-circle me-1"></i>
Inactive
</span>
{% endif %}
</dd>
<dt class="col-sm-4">Domain Sender:</dt>
<dd class="col-sm-8">
{% if user.can_send_as_domain %}
<span class="badge bg-success">
<i class="bi bi-check-circle me-1"></i>
Yes
</span>
{% else %}
<span class="badge bg-secondary">
<i class="bi bi-x-circle me-1"></i>
No
</span>
{% endif %}
</dd>
<dt class="col-sm-4">Created:</dt>
<dd class="col-sm-8">
<small class="text-muted">
{{ user.created_at.strftime('%Y-%m-%d %H:%M') }}
</small>
</dd>
</dl>
</div>
</div>
<div class="card mt-3">
<div class="card-header">
<h6 class="mb-0">
<i class="bi bi-shield-check me-2"></i>
User Permissions
</h6>
</div>
<div class="card-body">
<div class="alert alert-info mb-0">
<h6 class="alert-heading">
<i class="bi bi-info-circle me-2"></i>
Domain Sender Permission
</h6>
<ul class="mb-0 small">
<li><strong>Enabled:</strong> User can send emails using any address in their domain (e.g., admin@domain.com, support@domain.com)</li>
<li><strong>Disabled:</strong> User can only send emails from their own email address</li>
</ul>
</div>
</div>
</div>
</div>
</div>
{% endblock %}
{% block extra_js %}
<script>
document.addEventListener('DOMContentLoaded', function() {
// Auto-focus on email field
document.getElementById('email').focus();
// Password confirmation
const passwordField = document.getElementById('password');
let originalPlaceholder = passwordField.placeholder;
passwordField.addEventListener('focus', function() {
if (this.value === '') {
this.placeholder = 'Enter new password to change';
}
});
passwordField.addEventListener('blur', function() {
if (this.value === '') {
this.placeholder = originalPlaceholder;
}
});
});
</script>
{% endblock %}

View File

@@ -0,0 +1,173 @@
{% extends "base.html" %}
{% block title %}Error - SMTP Management{% endblock %}
{% block content %}
<div class="row">
<div class="col-12">
<div class="card border-danger">
<div class="card-header bg-danger text-white">
<div class="d-flex align-items-center">
<i class="fas fa-exclamation-triangle me-2"></i>
<h5 class="mb-0">Error Occurred</h5>
</div>
</div>
<div class="card-body">
{% if error_code %}
<div class="row mb-3">
<div class="col-sm-3"><strong>Error Code:</strong></div>
<div class="col-sm-9">
<span class="badge bg-danger fs-6">{{ error_code }}</span>
</div>
</div>
{% endif %}
{% if error_message %}
<div class="row mb-3">
<div class="col-sm-3"><strong>Message:</strong></div>
<div class="col-sm-9">
<div class="alert alert-danger mb-0">
{{ error_message }}
</div>
</div>
</div>
{% endif %}
{% if error_details %}
<div class="row mb-3">
<div class="col-sm-3"><strong>Details:</strong></div>
<div class="col-sm-9">
<div class="bg-dark text-light p-3 rounded">
<pre class="mb-0"><code>{{ error_details }}</code></pre>
</div>
</div>
</div>
{% endif %}
<div class="row mb-3">
<div class="col-sm-3"><strong>Timestamp:</strong></div>
<div class="col-sm-9">
<span class="text-muted">{{ current_time.strftime('%Y-%m-%d %H:%M:%S') if current_time else 'Unknown' }}</span>
</div>
</div>
<div class="row">
<div class="col-sm-3"><strong>Request URL:</strong></div>
<div class="col-sm-9">
<code>{{ request.url if request else 'Unknown' }}</code>
</div>
</div>
</div>
<div class="card-footer">
<div class="d-flex justify-content-between align-items-center">
<div>
<a href="{{ url_for('email.dashboard') }}" class="btn btn-primary">
<i class="fas fa-home me-1"></i>
Return to Dashboard
</a>
<button onclick="history.back()" class="btn btn-secondary">
<i class="fas fa-arrow-left me-1"></i>
Go Back
</button>
</div>
<div>
<button class="btn btn-outline-light" onclick="copyErrorDetails()">
<i class="fas fa-copy me-1"></i>
Copy Error Details
</button>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- Common Error Solutions -->
<div class="row mt-4">
<div class="col-12">
<div class="card">
<div class="card-header">
<h6 class="mb-0">
<i class="fas fa-lightbulb me-2"></i>
Common Solutions
</h6>
</div>
<div class="card-body">
<div class="row">
<div class="col-md-6">
<h6>Database Issues:</h6>
<ul class="text-muted small">
<li>Check database connection settings</li>
<li>Verify database tables exist</li>
<li>Check database permissions</li>
</ul>
</div>
<div class="col-md-6">
<h6>Configuration Issues:</h6>
<ul class="text-muted small">
<li>Verify settings.ini file exists</li>
<li>Check file permissions</li>
<li>Validate configuration values</li>
</ul>
</div>
</div>
<div class="row">
<div class="col-md-6">
<h6>Network Issues:</h6>
<ul class="text-muted small">
<li>Check firewall settings</li>
<li>Verify DNS resolution</li>
<li>Test network connectivity</li>
</ul>
</div>
<div class="col-md-6">
<h6>Permission Issues:</h6>
<ul class="text-muted small">
<li>Check file system permissions</li>
<li>Verify user authentication</li>
<li>Review access controls</li>
</ul>
</div>
</div>
</div>
</div>
</div>
</div>
<script>
function copyErrorDetails() {
const errorDetails = {
code: '{{ error_code or "Unknown" }}',
message: '{{ error_message or "No message" }}',
details: `{{ error_details or "No details" }}`,
timestamp: '{{ current_time.strftime("%Y-%m-%d %H:%M:%S") if current_time else "Unknown" }}',
url: '{{ request.url if request else "Unknown" }}'
};
const errorText = `Error Report:
Code: ${errorDetails.code}
Message: ${errorDetails.message}
Details: ${errorDetails.details}
Time: ${errorDetails.timestamp}
URL: ${errorDetails.url}`;
navigator.clipboard.writeText(errorText).then(() => {
// Show success message
const btn = event.target.closest('button');
const originalText = btn.innerHTML;
btn.innerHTML = '<i class="fas fa-check me-1"></i>Copied!';
btn.classList.remove('btn-outline-light');
btn.classList.add('btn-success');
setTimeout(() => {
btn.innerHTML = originalText;
btn.classList.remove('btn-success');
btn.classList.add('btn-outline-light');
}, 2000);
}).catch(err => {
console.error('Failed to copy: ', err);
alert('Failed to copy error details to clipboard');
});
}
</script>
{% endblock %}

View File

@@ -0,0 +1,229 @@
{% extends "base.html" %}
{% block title %}Whitelisted IPs - Email Server{% endblock %}
{% block content %}
<div class="container-fluid">
<div class="d-flex justify-content-between align-items-center mb-4">
<h2>
<i class="bi bi-router me-2"></i>
Whitelisted IP Addresses
</h2>
<a href="{{ url_for('email.add_ip') }}" class="btn btn-success">
<i class="bi bi-plus-circle me-2"></i>
Add IP Address
</a>
</div>
<div class="row">
<div class="col-lg-8">
<div class="card">
<div class="card-header">
<h5 class="mb-0">
<i class="bi bi-list me-2"></i>
Whitelisted IP Addresses
</h5>
</div>
<div class="card-body">
{% if ips %}
<div class="table-responsive">
<table class="table table-striped">
<thead>
<tr>
<th>IP Address</th>
<th>Domain</th>
<th>Status</th>
<th>Added</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
{% for ip, domain in ips %}
<tr>
<td>
<div class="fw-bold font-monospace">{{ ip.ip_address }}</div>
</td>
<td>
<span class="badge bg-secondary">{{ domain.domain_name }}</span>
</td>
<td>
{% if ip.is_active %}
<span class="badge bg-success">
<i class="bi bi-check-circle me-1"></i>
Active
</span>
{% else %}
<span class="badge bg-danger">
<i class="bi bi-x-circle me-1"></i>
Inactive
</span>
{% endif %}
</td>
<td>
<small class="text-muted">
{{ ip.created_at.strftime('%Y-%m-%d %H:%M') }}
</small>
</td>
<td>
<div class="btn-group" role="group">
<!-- Edit Button -->
<a href="{{ url_for('email.edit_ip', ip_id=ip.id) }}"
class="btn btn-outline-primary btn-sm"
title="Edit IP">
<i class="bi bi-pencil"></i>
</a>
<!-- Enable/Disable Button -->
{% if ip.is_active %}
<form method="post" action="{{ url_for('email.delete_ip', ip_id=ip.id) }}" class="d-inline">
<button type="submit"
class="btn btn-outline-warning btn-sm"
title="Disable IP"
onclick="return confirm('Disable {{ ip.ip_address }}?')">
<i class="bi bi-pause-circle"></i>
</button>
</form>
{% else %}
<form method="post" action="{{ url_for('email.enable_ip', ip_id=ip.id) }}" class="d-inline">
<button type="submit"
class="btn btn-outline-success btn-sm"
title="Enable IP"
onclick="return confirm('Enable {{ ip.ip_address }}?')">
<i class="bi bi-play-circle"></i>
</button>
</form>
{% endif %}
<!-- Permanent Remove Button -->
<form method="post" action="{{ url_for('email.remove_ip', ip_id=ip.id) }}" class="d-inline">
<button type="submit"
class="btn btn-outline-danger btn-sm"
title="Permanently Remove IP"
onclick="return confirm('Permanently remove {{ ip.ip_address }}? This cannot be undone!')">
<i class="bi bi-trash"></i>
</button>
</form>
</div>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
{% else %}
<div class="text-center py-5">
<i class="bi bi-router text-muted" style="font-size: 4rem;"></i>
<h4 class="text-muted mt-3">No IP Addresses Whitelisted</h4>
<p class="text-muted">Add IP addresses to allow authentication without username/password</p>
<a href="{{ url_for('email.add_ip') }}" class="btn btn-primary">
<i class="bi bi-plus-circle me-2"></i>
Add First IP Address
</a>
</div>
{% endif %}
</div>
</div>
</div>
<div class="col-lg-4">
<!-- Information Panel -->
<div class="card">
<div class="card-header">
<h6 class="mb-0">
<i class="bi bi-info-circle me-2"></i>
IP Whitelist Information
</h6>
</div>
<div class="card-body">
{% if ips %}
<ul class="list-unstyled mb-3">
<li class="mb-2">
<i class="bi bi-check-circle text-success me-2"></i>
<strong>Active IPs:</strong> {{ ips|selectattr('0.is_active')|list|length }}
</li>
<li class="mb-2">
<i class="bi bi-server text-info me-2"></i>
<strong>Domains covered:</strong> {{ ips|map(attribute='1.domain_name')|unique|list|length }}
</li>
<li>
<i class="bi bi-calendar text-muted me-2"></i>
<strong>Latest addition:</strong>
{% set latest = ips|map(attribute='0')|max(attribute='created_at') %}
{{ latest.created_at.strftime('%Y-%m-%d') if latest else 'N/A' }}
</li>
</ul>
{% endif %}
<div class="alert alert-info">
<h6 class="alert-heading">
<i class="bi bi-shield-check me-2"></i>
How IP Whitelisting Works
</h6>
<ul class="mb-0 small">
<li>Whitelisted IPs can send emails without username/password authentication</li>
<li>Each IP is associated with a specific domain</li>
<li>IP can only send emails for its authorized domain</li>
<li>Useful for server-to-server email sending</li>
</ul>
</div>
</div>
</div>
<!-- Current IP Detection -->
<div class="card mt-3">
<div class="card-header">
<h6 class="mb-0">
<i class="bi bi-geo-alt me-2"></i>
Your Current IP
</h6>
</div>
<div class="card-body">
<div class="text-center">
<div class="fw-bold font-monospace fs-5" id="current-ip">
<span class="spinner-border spinner-border-sm me-2"></span>
Detecting...
</div>
<button class="btn btn-outline-primary btn-sm mt-2" onclick="addCurrentIP()">
<i class="bi bi-plus-circle me-1"></i>
Add This IP
</button>
</div>
</div>
</div>
</div>
</div>
</div>
{% endblock %}
{% block extra_js %}
<script>
// Detect current IP address
async function detectCurrentIP() {
try {
const response = await fetch('https://api.ipify.org?format=json');
const data = await response.json();
document.getElementById('current-ip').innerHTML =
`<span class="text-primary">${data.ip}</span>`;
} catch (error) {
document.getElementById('current-ip').innerHTML =
'<span class="text-muted">Unable to detect</span>';
}
}
function addCurrentIP() {
const currentIPElement = document.getElementById('current-ip');
const ip = currentIPElement.textContent.trim();
if (ip && ip !== 'Detecting...' && ip !== 'Unable to detect') {
const url = new URL('{{ url_for("email.add_ip") }}', window.location.origin);
url.searchParams.set('ip', ip);
window.location.href = url.toString();
} else {
alert('Unable to detect current IP address');
}
}
// Auto-detect IP on page load
detectCurrentIP();
</script>
{% endblock %}

View File

@@ -0,0 +1,322 @@
{% extends "base.html" %}
{% block title %}Logs - Email Server{% endblock %}
{% block extra_css %}
<style>
.log-entry {
border-left: 4px solid var(--bs-border-color);
padding: 0.75rem;
margin-bottom: 0.5rem;
background-color: var(--bs-body-bg);
border-radius: 0.375rem;
}
.log-email { border-left-color: #0d6efd; }
.log-auth { border-left-color: #198754; }
.log-error { border-left-color: #dc3545; }
.log-success { border-left-color: #198754; }
.log-failed { border-left-color: #dc3545; }
.log-content {
font-family: 'Courier New', monospace;
font-size: 0.875rem;
background-color: var(--bs-gray-100);
border-radius: 0.25rem;
padding: 0.5rem;
max-height: 150px;
overflow-y: auto;
}
</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-journal-text me-2"></i>
Server Logs
</h2>
<div class="btn-group">
<a href="{{ url_for('email.logs', type='all') }}"
class="btn {{ 'btn-primary' if filter_type == 'all' else 'btn-outline-primary' }}">
<i class="bi bi-list-ul me-1"></i>
All Logs
</a>
<a href="{{ url_for('email.logs', type='emails') }}"
class="btn {{ 'btn-primary' if filter_type == 'emails' else 'btn-outline-primary' }}">
<i class="bi bi-envelope me-1"></i>
Email Logs
</a>
<a href="{{ url_for('email.logs', type='auth') }}"
class="btn {{ 'btn-primary' if filter_type == 'auth' else 'btn-outline-primary' }}">
<i class="bi bi-shield-lock me-1"></i>
Auth Logs
</a>
</div>
</div>
<div class="row">
<div class="col-12">
<div class="card">
<div class="card-header d-flex justify-content-between align-items-center">
<h5 class="mb-0">
{% if filter_type == 'emails' %}
<i class="bi bi-envelope me-2"></i>
Email Activity
{% elif filter_type == 'auth' %}
<i class="bi bi-shield-lock me-2"></i>
Authentication Activity
{% else %}
<i class="bi bi-list-ul me-2"></i>
Recent Activity
{% endif %}
</h5>
<button class="btn btn-outline-secondary btn-sm" onclick="refreshLogs()">
<i class="bi bi-arrow-clockwise me-1"></i>
Refresh
</button>
</div>
<div class="card-body">
{% if logs %}
{% if filter_type == 'all' %}
<!-- Combined logs view -->
{% for log_entry in logs %}
{% if log_entry.type == 'email' %}
{% set log = log_entry.data %}
<div class="log-entry log-email log-{{ 'success' if log.status == 'relayed' else 'failed' }}">
<div class="d-flex justify-content-between align-items-start mb-2">
<div>
<span class="badge bg-primary me-2">EMAIL</span>
<strong>{{ log.mail_from }}</strong> → {{ log.rcpt_tos }}
{% if log.dkim_signed %}
<span class="badge bg-success ms-2">
<i class="bi bi-shield-check me-1"></i>
DKIM
</span>
{% endif %}
</div>
<small class="text-muted">{{ log.created_at.strftime('%Y-%m-%d %H:%M:%S') }}</small>
</div>
<div class="row">
<div class="col-md-6">
<strong>Status:</strong>
{% if log.status == 'relayed' %}
<span class="text-success">Sent Successfully</span>
{% else %}
<span class="text-danger">Failed</span>
{% endif %}
</div>
<div class="col-md-6">
<strong>Message ID:</strong> <code>{{ log.message_id }}</code>
</div>
</div>
{% if log.subject %}
<div class="mt-2">
<strong>Subject:</strong> {{ log.subject }}
</div>
{% endif %}
</div>
{% else %}
{% set log = log_entry.data %}
<div class="log-entry log-auth log-{{ 'success' if log.success else 'failed' }}">
<div class="d-flex justify-content-between align-items-start mb-2">
<div>
<span class="badge bg-success me-2">AUTH</span>
<strong>{{ log.identifier }}</strong>
<span class="badge {{ 'bg-success' if log.success else 'bg-danger' }} ms-2">
{{ 'Success' if log.success else 'Failed' }}
</span>
</div>
<small class="text-muted">{{ log.created_at.strftime('%Y-%m-%d %H:%M:%S') }}</small>
</div>
<div class="row">
<div class="col-md-6">
<strong>Type:</strong> {{ log.auth_type.upper() }}
</div>
<div class="col-md-6">
<strong>IP:</strong> <code>{{ log.ip_address or 'N/A' }}</code>
</div>
</div>
{% if log.message %}
<div class="mt-2">
<strong>Message:</strong> {{ log.message }}
</div>
{% endif %}
</div>
{% endif %}
{% endfor %}
{% elif filter_type == 'emails' %}
<!-- Email logs only -->
{% for log in logs %}
<div class="log-entry log-email log-{{ 'success' if log.status == 'relayed' else 'failed' }}">
<div class="d-flex justify-content-between align-items-start mb-2">
<div>
<strong>{{ log.mail_from }}</strong> → {{ log.rcpt_tos }}
{% if log.dkim_signed %}
<span class="badge bg-success ms-2">
<i class="bi bi-shield-check me-1"></i>
DKIM
</span>
{% endif %}
</div>
<small class="text-muted">{{ log.created_at.strftime('%Y-%m-%d %H:%M:%S') }}</small>
</div>
<div class="row">
<div class="col-md-3">
<strong>Status:</strong>
{% if log.status == 'relayed' %}
<span class="text-success">Sent</span>
{% else %}
<span class="text-danger">Failed</span>
{% endif %}
</div>
<div class="col-md-3">
<strong>Peer:</strong> <code>{{ log.peer }}</code>
</div>
<div class="col-md-6">
<strong>Message ID:</strong> <code>{{ log.message_id }}</code>
</div>
</div>
{% if log.subject %}
<div class="mt-2">
<strong>Subject:</strong> {{ log.subject }}
</div>
{% endif %}
{% if log.content and log.content|length > 50 %}
<div class="mt-2">
<button class="btn btn-outline-secondary btn-sm" type="button"
data-bs-toggle="collapse"
data-bs-target="#content-{{ log.id }}">
<i class="bi bi-eye me-1"></i>
View Content
</button>
<div class="collapse mt-2" id="content-{{ log.id }}">
<div class="log-content">{{ log.content }}</div>
</div>
</div>
{% endif %}
</div>
{% endfor %}
{% else %}
<!-- Auth logs only -->
{% for log in logs %}
<div class="log-entry log-auth log-{{ 'success' if log.success else 'failed' }}">
<div class="d-flex justify-content-between align-items-start mb-2">
<div>
<strong>{{ log.identifier }}</strong>
<span class="badge {{ 'bg-success' if log.success else 'bg-danger' }} ms-2">
{{ 'Success' if log.success else 'Failed' }}
</span>
</div>
<small class="text-muted">{{ log.created_at.strftime('%Y-%m-%d %H:%M:%S') }}</small>
</div>
<div class="row">
<div class="col-md-4">
<strong>Type:</strong> {{ log.auth_type.upper() }}
</div>
<div class="col-md-4">
<strong>IP:</strong> <code>{{ log.ip_address or 'N/A' }}</code>
</div>
<div class="col-md-4">
<strong>Result:</strong>
{% if log.success %}
<span class="text-success">Authenticated</span>
{% else %}
<span class="text-danger">Rejected</span>
{% endif %}
</div>
</div>
{% if log.message %}
<div class="mt-2">
<strong>Details:</strong> {{ log.message }}
</div>
{% endif %}
</div>
{% endfor %}
{% endif %}
<!-- Pagination -->
{% if has_prev or has_next %}
<nav aria-label="Log pagination" class="mt-4">
<ul class="pagination justify-content-center">
{% if has_prev %}
<li class="page-item">
<a class="page-link" href="{{ url_for('email.logs', type=filter_type, page=page-1) }}">
<i class="bi bi-chevron-left"></i>
Previous
</a>
</li>
{% endif %}
<li class="page-item active">
<span class="page-link">Page {{ page }}</span>
</li>
{% if has_next %}
<li class="page-item">
<a class="page-link" href="{{ url_for('email.logs', type=filter_type, page=page+1) }}">
Next
<i class="bi bi-chevron-right"></i>
</a>
</li>
{% endif %}
</ul>
</nav>
{% endif %}
{% else %}
<div class="text-center py-5">
<i class="bi bi-journal-text text-muted" style="font-size: 4rem;"></i>
<h4 class="text-muted mt-3">No Logs Found</h4>
<p class="text-muted">
{% if filter_type == 'emails' %}
No email activity has been logged yet.
{% elif filter_type == 'auth' %}
No authentication attempts have been logged yet.
{% else %}
No activity has been logged yet.
{% endif %}
</p>
</div>
{% endif %}
</div>
</div>
</div>
</div>
</div>
{% endblock %}
{% block extra_js %}
<script>
function refreshLogs() {
window.location.reload();
}
// Auto-refresh every 30 seconds
setInterval(function() {
// Only auto-refresh if the user is viewing the page
if (document.visibilityState === 'visible') {
const button = document.querySelector('[onclick="refreshLogs()"]');
if (button) {
// Add visual indicator that refresh is happening
const originalText = button.innerHTML;
button.innerHTML = '<i class="bi bi-arrow-clockwise me-1 spin"></i>Refreshing...';
setTimeout(() => {
window.location.reload();
}, 1000);
}
}
}, 30000);
// Add CSS for spinning icon
const style = document.createElement('style');
style.textContent = `
@keyframes spin {
from { transform: rotate(0deg); }
to { transform: rotate(360deg); }
}
.spin {
animation: spin 1s linear infinite;
}
`;
document.head.appendChild(style);
</script>
{% endblock %}

View File

@@ -0,0 +1,355 @@
{% extends "base.html" %}
{% block title %}Server Settings - Email Server{% endblock %}
{% block extra_css %}
<style>
.setting-section {
border-left: 4px solid var(--bs-primary);
padding-left: 1rem;
margin-bottom: 2rem;
}
.setting-description {
font-size: 0.875rem;
color: var(--bs-secondary);
margin-bottom: 0.5rem;
}
</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-sliders me-2"></i>
Server Settings
</h2>
<div class="btn-group">
<button type="button" class="btn btn-outline-warning" onclick="resetToDefaults()">
<i class="bi bi-arrow-clockwise me-2"></i>
Reset to Defaults
</button>
<button type="button" class="btn btn-outline-info" onclick="exportSettings()">
<i class="bi bi-download me-2"></i>
Export Config
</button>
</div>
</div>
<form method="POST" action="{{ url_for('email.update_settings') }}">
<!-- 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
</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>
</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>
</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>
</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>
</div>
<!-- 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
</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>
</div>
</div>
</div>
<!-- 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
</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>
</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>
</div>
</div>
</div>
</div>
</div>
<!-- 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
</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>
</div>
</div>
</div>
<!-- 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
</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>
</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>
</div>
</div>
</div>
</div>
</div>
<!-- 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
</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>
</div>
</div>
</div>
<!-- Save Button -->
<div class="d-flex justify-content-between align-items-center">
<div class="alert alert-warning d-flex align-items-center mb-0">
<i class="bi bi-exclamation-triangle me-2"></i>
<small>Server restart required after changing settings</small>
</div>
<button type="submit" class="btn btn-primary btn-lg">
<i class="bi bi-save me-2"></i>
Save Settings
</button>
</div>
</form>
</div>
<!-- Reset Confirmation Modal -->
<div class="modal fade" id="resetModal" tabindex="-1">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">Reset Settings</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
<p>Are you sure you want to reset all settings to their default values?</p>
<p class="text-warning"><strong>Warning:</strong> This action cannot be undone.</p>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
<button type="button" class="btn btn-warning" onclick="confirmReset()">Reset Settings</button>
</div>
</div>
</div>
</div>
{% endblock %}
{% block extra_js %}
<script>
function resetToDefaults() {
new bootstrap.Modal(document.getElementById('resetModal')).show();
}
function confirmReset() {
// This would need to be implemented as a separate endpoint
// For now, just redirect to a reset URL
window.location.href = '{{ url_for("email.settings") }}?reset=true';
}
function exportSettings() {
// Create a downloadable config file
const settings = {};
const formData = new FormData(document.querySelector('form'));
for (let [key, value] of formData.entries()) {
const [section, setting] = key.split('.');
if (!settings[section]) {
settings[section] = {};
}
settings[section][setting] = value;
}
const configText = generateConfigFile(settings);
downloadFile('settings.ini', configText);
}
function generateConfigFile(settings) {
let config = '';
for (const [section, values] of Object.entries(settings)) {
config += `[${section}]\n`;
for (const [key, value] of Object.entries(values)) {
config += `${key} = ${value}\n`;
}
config += '\n';
}
return config;
}
function downloadFile(filename, content) {
const element = document.createElement('a');
element.setAttribute('href', 'data:text/plain;charset=utf-8,' + encodeURIComponent(content));
element.setAttribute('download', filename);
element.style.display = 'none';
document.body.appendChild(element);
element.click();
document.body.removeChild(element);
}
// Form validation
document.querySelector('form').addEventListener('submit', function(e) {
// Basic validation
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);
if (port < 1 || port > 65535) {
e.preventDefault();
alert(`Invalid port number: ${port}. Must be between 1 and 65535.`);
input.focus();
return;
}
}
// Check if ports are different
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.');
return;
}
});
</script>
{% endblock %}

View File

@@ -0,0 +1,187 @@
<!-- Sidebar Navigation -->
<nav class="sidebar bg-dark border-end border-secondary position-fixed h-100" style="width: var(--sidebar-width); z-index: 1000;">
<div class="d-flex flex-column h-100">
<!-- Sidebar header -->
<div class="p-3 border-bottom border-secondary">
<h5 class="text-white mb-0">
<i class="bi bi-server me-2"></i>
SMTP Server
</h5>
<small class="text-muted">Management Console</small>
</div>
<!-- Navigation menu -->
<div class="flex-grow-1 overflow-auto">
<ul class="nav nav-pills flex-column p-3">
<!-- Dashboard -->
<li class="nav-item mb-2">
<a href="{{ url_for('email.dashboard') }}"
class="nav-link text-white {{ 'active' if request.endpoint == 'email.dashboard' else '' }}">
<i class="bi bi-speedometer2 me-2"></i>
Dashboard
</a>
</li>
<!-- Domains Section -->
<li class="nav-item mb-2">
<h6 class="text-muted text-uppercase small mb-2 mt-3">
<i class="bi bi-globe me-1"></i>
Domain Management
</h6>
</li>
<li class="nav-item mb-1">
<a href="{{ url_for('email.domains_list') }}"
class="nav-link text-white {{ 'active' if request.endpoint in ['email.domains_list', 'email.add_domain'] else '' }}">
<i class="bi bi-list-ul me-2"></i>
Domains
<span class="badge bg-secondary ms-auto">{{ domain_count if domain_count is defined else '' }}</span>
</a>
</li>
<!-- Authentication Section -->
<li class="nav-item mb-2">
<h6 class="text-muted text-uppercase small mb-2 mt-3">
<i class="bi bi-shield-lock me-1"></i>
Authentication
</h6>
</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 '' }}">
<i class="bi bi-people me-2"></i>
Users
<span class="badge bg-secondary ms-auto">{{ user_count if user_count is defined else '' }}</span>
</a>
</li>
<li class="nav-item mb-1">
<a href="{{ url_for('email.ips_list') }}"
class="nav-link text-white {{ 'active' if request.endpoint in ['email.ips_list', 'email.add_ip'] else '' }}">
<i class="bi bi-router me-2"></i>
Whitelisted IPs
<span class="badge bg-secondary ms-auto">{{ ip_count if ip_count is defined else '' }}</span>
</a>
</li>
<!-- DKIM Section -->
<li class="nav-item mb-2">
<h6 class="text-muted text-uppercase small mb-2 mt-3">
<i class="bi bi-key me-1"></i>
Email Security
</h6>
</li>
<li class="nav-item mb-1">
<a href="{{ url_for('email.dkim_list') }}"
class="nav-link text-white {{ 'active' if request.endpoint == 'email.dkim_list' else '' }}">
<i class="bi bi-shield-check me-2"></i>
DKIM Keys
<span class="badge bg-secondary ms-auto">{{ dkim_count if dkim_count is defined else '' }}</span>
</a>
</li>
<!-- Configuration Section -->
<li class="nav-item mb-2">
<h6 class="text-muted text-uppercase small mb-2 mt-3">
<i class="bi bi-gear me-1"></i>
Configuration
</h6>
</li>
<li class="nav-item mb-1">
<a href="{{ url_for('email.settings') }}"
class="nav-link text-white {{ 'active' if request.endpoint == 'email.settings' else '' }}">
<i class="bi bi-sliders me-2"></i>
Server Settings
</a>
</li>
<!-- Monitoring Section -->
<li class="nav-item mb-2">
<h6 class="text-muted text-uppercase small mb-2 mt-3">
<i class="bi bi-activity me-1"></i>
Monitoring
</h6>
</li>
<li class="nav-item mb-1">
<a href="{{ url_for('email.logs') }}"
class="nav-link text-white {{ 'active' if request.endpoint == 'email.logs' else '' }}">
<i class="bi bi-journal-text me-2"></i>
Logs & Activity
</a>
</li>
</ul>
</div>
<!-- Sidebar footer -->
<div class="p-3 border-top border-secondary">
<div class="d-flex align-items-center">
<div class="flex-grow-1">
<small class="text-muted d-block">Server Status</small>
<small class="text-success">
<i class="bi bi-circle-fill me-1" style="font-size: 0.5rem;"></i>
Online
</small>
</div>
<button class="btn btn-outline-secondary btn-sm" title="Refresh Status">
<i class="bi bi-arrow-clockwise"></i>
</button>
</div>
</div>
</div>
</nav>
<style>
.sidebar .nav-link {
border-radius: 0.375rem;
padding: 0.75rem 1rem;
margin-bottom: 0.25rem;
transition: all 0.2s ease;
}
.sidebar .nav-link:hover {
background-color: rgba(255, 255, 255, 0.1);
transform: translateX(4px);
}
.sidebar .nav-link.active {
background-color: #0d6efd;
color: white !important;
}
.sidebar .nav-link.active:hover {
background-color: #0b5ed7;
}
.sidebar h6 {
font-size: 0.75rem;
font-weight: 600;
letter-spacing: 0.05em;
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
padding-bottom: 0.5rem;
margin-bottom: 1rem !important;
}
.sidebar .badge {
font-size: 0.7rem;
}
/* Responsive design */
@media (max-width: 768px) {
.sidebar {
transform: translateX(-100%);
transition: transform 0.3s ease;
}
.sidebar.show {
transform: translateX(0);
}
.content-area {
margin-left: 0 !important;
}
}
</style>

View File

@@ -0,0 +1,195 @@
{% extends "base.html" %}
{% block title %}Users - Email Server Management{% endblock %}
{% block page_title %}User Management{% endblock %}
{% block content %}
<div class="d-flex justify-content-between align-items-center mb-4">
<h2>
<i class="bi bi-people me-2"></i>
Users
</h2>
<a href="{{ url_for('email.add_user') }}" class="btn btn-primary">
<i class="bi bi-person-plus me-2"></i>
Add User
</a>
</div>
<div class="card">
<div class="card-header">
<h5 class="mb-0">
<i class="bi bi-list-ul me-2"></i>
All Users
</h5>
</div>
<div class="card-body p-0">
{% if users %}
<div class="table-responsive">
<table class="table table-dark table-hover mb-0">
<thead>
<tr>
<th>Email</th>
<th>Domain</th>
<th>Permissions</th>
<th>Status</th>
<th>Created</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
{% for user, domain in users %}
<tr>
<td>
<div class="fw-bold">{{ user.email }}</div>
</td>
<td>
<span class="badge bg-secondary">{{ domain.domain_name }}</span>
</td>
<td>
{% if user.can_send_as_domain %}
<span class="badge bg-warning">
<i class="bi bi-star me-1"></i>
Domain Admin
</span>
<br>
<small class="text-muted">Can send as *@{{ domain.domain_name }}</small>
{% else %}
<span class="badge bg-info">
<i class="bi bi-person me-1"></i>
User
</span>
<br>
<small class="text-muted">Can only send as {{ user.email }}</small>
{% endif %}
</td>
<td>
{% if user.is_active %}
<span class="badge bg-success">
<i class="bi bi-check-circle me-1"></i>
Active
</span>
{% else %}
<span class="badge bg-danger">
<i class="bi bi-x-circle me-1"></i>
Inactive
</span>
{% endif %}
</td>
<td>
<small class="text-muted">
{{ user.created_at.strftime('%Y-%m-%d %H:%M') }}
</small>
</td>
<td>
<div class="btn-group" role="group">
<!-- Edit Button -->
<a href="{{ url_for('email.edit_user', user_id=user.id) }}"
class="btn btn-outline-primary btn-sm"
title="Edit User">
<i class="bi bi-pencil"></i>
</a>
<!-- Enable/Disable Button -->
{% if user.is_active %}
<form method="post" action="{{ url_for('email.delete_user', user_id=user.id) }}" class="d-inline">
<button type="submit"
class="btn btn-outline-warning btn-sm"
title="Disable User"
onclick="return confirm('Disable user {{ user.email }}?')">
<i class="bi bi-pause-circle"></i>
</button>
</form>
{% else %}
<form method="post" action="{{ url_for('email.enable_user', user_id=user.id) }}" class="d-inline">
<button type="submit"
class="btn btn-outline-success btn-sm"
title="Enable User"
onclick="return confirm('Enable user {{ user.email }}?')">
<i class="bi bi-play-circle"></i>
</button>
</form>
{% endif %}
<!-- Permanent Remove Button -->
<form method="post" action="{{ url_for('email.remove_user', user_id=user.id) }}" class="d-inline">
<button type="submit"
class="btn btn-outline-danger btn-sm"
title="Permanently Remove User"
onclick="return confirm('Permanently remove user {{ user.email }}? This cannot be undone!')">
<i class="bi bi-trash"></i>
</button>
</form>
</div>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
{% else %}
<div class="text-center py-5">
<i class="bi bi-people text-muted" style="font-size: 4rem;"></i>
<h4 class="text-muted mt-3">No users configured</h4>
<p class="text-muted">Add users to enable username/password authentication</p>
<a href="{{ url_for('email.add_user') }}" class="btn btn-primary">
<i class="bi bi-person-plus me-2"></i>
Add Your First User
</a>
</div>
{% endif %}
</div>
</div>
{% if users %}
<div class="row mt-4">
<div class="col-md-6">
<div class="card">
<div class="card-header">
<h6 class="mb-0">
<i class="bi bi-info-circle me-2"></i>
User Statistics
</h6>
</div>
<div class="card-body">
<ul class="list-unstyled mb-0">
<li class="mb-2">
<i class="bi bi-check-circle text-success me-2"></i>
<strong>Active users:</strong> {{ users|selectattr('0.is_active')|list|length }}
</li>
<li class="mb-2">
<i class="bi bi-star text-warning me-2"></i>
<strong>Domain admins:</strong> {{ users|selectattr('0.can_send_as_domain')|list|length }}
</li>
<li>
<i class="bi bi-person text-info me-2"></i>
<strong>Regular users:</strong> {{ users|rejectattr('0.can_send_as_domain')|list|length }}
</li>
</ul>
</div>
</div>
</div>
<div class="col-md-6">
<div class="card">
<div class="card-header">
<h6 class="mb-0">
<i class="bi bi-lightbulb me-2"></i>
Permission Levels
</h6>
</div>
<div class="card-body">
<ul class="list-unstyled mb-0">
<li class="mb-2">
<span class="badge bg-warning me-2">Domain Admin</span>
Can send as any email address in their domain
</li>
<li>
<span class="badge bg-info me-2">Regular User</span>
Can only send as their own email address
</li>
</ul>
</div>
</div>
</div>
</div>
{% endif %}
{% endblock %}