web_UI working - needs more tweaking
This commit is contained in:
6
app.py
6
app.py
@@ -34,7 +34,7 @@ from email_server.tool_box import get_logger
|
||||
from email_server.dkim_manager import DKIMManager
|
||||
|
||||
# Import Flask frontend
|
||||
from email_frontend.blueprint import email_bp
|
||||
from email_server.server_web_ui.routes import email_bp
|
||||
|
||||
logger = get_logger()
|
||||
|
||||
@@ -69,8 +69,8 @@ class SMTPServerApp:
|
||||
def create_flask_app(self):
|
||||
"""Create and configure the Flask application"""
|
||||
app = Flask(__name__,
|
||||
static_folder='email_frontend/static',
|
||||
template_folder='email_frontend/templates')
|
||||
static_folder='email_server/server_web_ui/static',
|
||||
template_folder='email_server/server_web_ui/templates')
|
||||
|
||||
# Flask configuration
|
||||
app.config.update({
|
||||
|
||||
@@ -1,209 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Example Flask Application demonstrating SMTP Management Frontend
|
||||
This example shows how to integrate the email_frontend Blueprint
|
||||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
from datetime import datetime
|
||||
from flask import Flask, render_template, request, redirect, url_for, flash, jsonify
|
||||
from flask_sqlalchemy import SQLAlchemy
|
||||
|
||||
# Add the project root to Python path
|
||||
sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||||
|
||||
# Import the SMTP server models and utilities
|
||||
try:
|
||||
from database import Database, Domain, User, WhitelistedIP, DKIMKey, EmailLog, AuthLog
|
||||
from email_frontend.blueprint import email_bp
|
||||
except ImportError as e:
|
||||
print(f"Error importing modules: {e}")
|
||||
print("Make sure you're running this from the SMTP_Server directory")
|
||||
sys.exit(1)
|
||||
|
||||
def create_app(config_file='settings.ini'):
|
||||
"""Create and configure the Flask application."""
|
||||
app = Flask(__name__)
|
||||
|
||||
# Basic Flask configuration
|
||||
app.config['SECRET_KEY'] = 'your-secret-key-change-this-in-production'
|
||||
app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///smtp_server.db'
|
||||
app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False
|
||||
|
||||
# Initialize database
|
||||
db = SQLAlchemy(app)
|
||||
|
||||
# Create database tables if they don't exist
|
||||
with app.app_context():
|
||||
db.create_all()
|
||||
|
||||
# Register the email management blueprint
|
||||
app.register_blueprint(email_bp, url_prefix='/email')
|
||||
|
||||
# Main application routes
|
||||
@app.route('/')
|
||||
def index():
|
||||
"""Main application dashboard."""
|
||||
return redirect(url_for('email.dashboard'))
|
||||
|
||||
@app.route('/health')
|
||||
def health_check():
|
||||
"""Simple health check endpoint."""
|
||||
return jsonify({
|
||||
'status': 'healthy',
|
||||
'timestamp': datetime.utcnow().isoformat(),
|
||||
'service': 'SMTP Management Frontend'
|
||||
})
|
||||
|
||||
# Error handlers
|
||||
@app.errorhandler(404)
|
||||
def not_found_error(error):
|
||||
"""Handle 404 errors."""
|
||||
return render_template('error.html',
|
||||
error_code=404,
|
||||
error_message="Page not found",
|
||||
error_details="The requested page could not be found."), 404
|
||||
|
||||
@app.errorhandler(500)
|
||||
def internal_error(error):
|
||||
"""Handle 500 errors."""
|
||||
return render_template('error.html',
|
||||
error_code=500,
|
||||
error_message="Internal server error",
|
||||
error_details=str(error)), 500
|
||||
|
||||
@app.errorhandler(403)
|
||||
def forbidden_error(error):
|
||||
"""Handle 403 errors."""
|
||||
return render_template('error.html',
|
||||
error_code=403,
|
||||
error_message="Access forbidden",
|
||||
error_details="You don't have permission to access this resource."), 403
|
||||
|
||||
# Context processors for templates
|
||||
@app.context_processor
|
||||
def utility_processor():
|
||||
"""Add utility functions to template context."""
|
||||
return {
|
||||
'moment': datetime,
|
||||
'len': len,
|
||||
'enumerate': enumerate,
|
||||
'zip': zip,
|
||||
'str': str,
|
||||
'int': int,
|
||||
}
|
||||
|
||||
return app
|
||||
|
||||
def init_sample_data():
|
||||
"""Initialize the database with sample data for testing."""
|
||||
try:
|
||||
# Initialize database connection
|
||||
db = Database('settings.ini')
|
||||
|
||||
# Add sample domains
|
||||
sample_domains = [
|
||||
'example.com',
|
||||
'testdomain.org',
|
||||
'mydomain.net'
|
||||
]
|
||||
|
||||
for domain_name in sample_domains:
|
||||
if not db.get_domain(domain_name):
|
||||
domain = Domain(domain_name)
|
||||
db.add_domain(domain)
|
||||
print(f"Added sample domain: {domain_name}")
|
||||
|
||||
# Add sample users
|
||||
sample_users = [
|
||||
('admin@example.com', 'example.com', 'admin123'),
|
||||
('user@example.com', 'example.com', 'user123'),
|
||||
('test@testdomain.org', 'testdomain.org', 'test123')
|
||||
]
|
||||
|
||||
for email, domain, password in sample_users:
|
||||
if not db.get_user(email):
|
||||
user = User(email, domain, password)
|
||||
db.add_user(user)
|
||||
print(f"Added sample user: {email}")
|
||||
|
||||
# Add sample whitelisted IPs
|
||||
sample_ips = [
|
||||
('127.0.0.1', 'example.com', 'localhost'),
|
||||
('192.168.1.0/24', 'example.com', 'local network'),
|
||||
('10.0.0.0/8', 'testdomain.org', 'private network')
|
||||
]
|
||||
|
||||
for ip, domain, description in sample_ips:
|
||||
if not db.get_whitelisted_ip(ip, domain):
|
||||
whitelisted_ip = WhitelistedIP(ip, domain, description)
|
||||
db.add_whitelisted_ip(whitelisted_ip)
|
||||
print(f"Added sample whitelisted IP: {ip} for {domain}")
|
||||
|
||||
print("Sample data initialized successfully!")
|
||||
|
||||
except Exception as e:
|
||||
print(f"Error initializing sample data: {e}")
|
||||
|
||||
def main():
|
||||
"""Main function to run the example application."""
|
||||
import argparse
|
||||
|
||||
parser = argparse.ArgumentParser(description='SMTP Management Frontend Example')
|
||||
parser.add_argument('--host', default='127.0.0.1', help='Host to bind to')
|
||||
parser.add_argument('--port', type=int, default=5000, help='Port to bind to')
|
||||
parser.add_argument('--debug', action='store_true', help='Enable debug mode')
|
||||
parser.add_argument('--init-data', action='store_true', help='Initialize sample data')
|
||||
parser.add_argument('--config', default='settings.ini', help='Configuration file path')
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
# Initialize sample data if requested
|
||||
if args.init_data:
|
||||
print("Initializing sample data...")
|
||||
init_sample_data()
|
||||
return
|
||||
|
||||
# Create Flask application
|
||||
app = create_app(args.config)
|
||||
|
||||
print(f"""
|
||||
SMTP Management Frontend Example
|
||||
================================
|
||||
|
||||
Starting server on http://{args.host}:{args.port}
|
||||
|
||||
Available routes:
|
||||
- / -> Dashboard (redirects to /email/dashboard)
|
||||
- /email/dashboard -> Main dashboard
|
||||
- /email/domains -> Domain management
|
||||
- /email/users -> User management
|
||||
- /email/ips -> IP whitelist management
|
||||
- /email/dkim -> DKIM management
|
||||
- /email/settings -> Server settings
|
||||
- /email/logs -> Email and authentication logs
|
||||
- /health -> Health check endpoint
|
||||
|
||||
Debug mode: {'ON' if args.debug else 'OFF'}
|
||||
|
||||
To initialize sample data, run:
|
||||
python example_app.py --init-data
|
||||
""")
|
||||
|
||||
# Run the Flask application
|
||||
try:
|
||||
app.run(
|
||||
host=args.host,
|
||||
port=args.port,
|
||||
debug=args.debug,
|
||||
threaded=True
|
||||
)
|
||||
except KeyboardInterrupt:
|
||||
print("\nShutting down gracefully...")
|
||||
except Exception as e:
|
||||
print(f"Error starting server: {e}")
|
||||
sys.exit(1)
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
@@ -1,318 +0,0 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block title %}DKIM Keys - Email Server{% endblock %}
|
||||
|
||||
{% block extra_css %}
|
||||
<style>
|
||||
.dns-record {
|
||||
font-family: 'Courier New', monospace;
|
||||
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">
|
||||
<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>
|
||||
<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>
|
||||
</div>
|
||||
<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>
|
||||
{% 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 and selector from the card
|
||||
const domainName = domainCard.querySelector('h5').textContent.trim().split('\n')[0].trim();
|
||||
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);
|
||||
});
|
||||
}
|
||||
</script>
|
||||
{% endblock %}
|
||||
@@ -1,174 +0,0 @@
|
||||
{% 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">
|
||||
<a href="{{ url_for('email.dkim_list') }}#domain-{{ domain.id }}"
|
||||
class="btn btn-outline-info"
|
||||
title="Manage DKIM">
|
||||
<i class="bi bi-key"></i>
|
||||
</a>
|
||||
{% if domain.is_active %}
|
||||
<form method="post" action="{{ url_for('email.delete_domain', domain_id=domain.id) }}" class="d-inline">
|
||||
<button type="submit"
|
||||
class="btn btn-outline-danger"
|
||||
data-confirm="Are you sure you want to deactivate domain '{{ domain.domain_name }}'?"
|
||||
title="Deactivate Domain">
|
||||
<i class="bi bi-trash"></i>
|
||||
</button>
|
||||
</form>
|
||||
{% endif %}
|
||||
</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 %}
|
||||
@@ -1,112 +0,0 @@
|
||||
{% 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 %}
|
||||
@@ -1,228 +0,0 @@
|
||||
{% 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 %}
|
||||
@@ -1,176 +0,0 @@
|
||||
{% 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 %}
|
||||
@@ -1,285 +0,0 @@
|
||||
{% 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.timestamp.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 %}
|
||||
@@ -1,173 +0,0 @@
|
||||
{% 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">{{ moment().format('YYYY-MM-DD HH:mm:ss') }}</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_management.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: '{{ moment().format("YYYY-MM-DD HH:mm:ss") }}',
|
||||
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 %}
|
||||
@@ -1,203 +0,0 @@
|
||||
{% 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>
|
||||
{% 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-danger btn-sm"
|
||||
onclick="return confirm('Remove {{ ip.ip_address }} from whitelist?')">
|
||||
<i class="bi bi-trash"></i>
|
||||
</button>
|
||||
</form>
|
||||
{% else %}
|
||||
<span class="text-muted">
|
||||
<i class="bi bi-dash"></i>
|
||||
</span>
|
||||
{% endif %}
|
||||
</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.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 %}
|
||||
@@ -1,170 +0,0 @@
|
||||
{% 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>
|
||||
{% 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-danger btn-sm"
|
||||
data-confirm="Are you sure you want to deactivate user '{{ user.email }}'?"
|
||||
title="Deactivate User">
|
||||
<i class="bi bi-trash"></i>
|
||||
</button>
|
||||
</form>
|
||||
{% else %}
|
||||
<span class="text-muted">
|
||||
<i class="bi bi-dash-circle"></i>
|
||||
</span>
|
||||
{% endif %}
|
||||
</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 %}
|
||||
@@ -1,322 +0,0 @@
|
||||
{% 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 %}
|
||||
@@ -1,355 +0,0 @@
|
||||
{% 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 %}
|
||||
@@ -103,6 +103,7 @@ def generate_spf_record(domain: str, public_ip: str, existing_spf: str = None) -
|
||||
spf_parts = ['v=spf1'] + base_mechanisms + ['~all']
|
||||
return ' '.join(spf_parts)
|
||||
|
||||
# Dashboard and Main Routes
|
||||
@email_bp.route('/')
|
||||
def dashboard():
|
||||
"""Main dashboard showing overview of the email server."""
|
||||
@@ -128,6 +129,7 @@ def dashboard():
|
||||
finally:
|
||||
session.close()
|
||||
|
||||
# Domain Management Routes
|
||||
@email_bp.route('/domains')
|
||||
def domains_list():
|
||||
"""List all domains."""
|
||||
@@ -180,7 +182,7 @@ def add_domain():
|
||||
|
||||
@email_bp.route('/domains/<int:domain_id>/delete', methods=['POST'])
|
||||
def delete_domain(domain_id: int):
|
||||
"""Delete domain."""
|
||||
"""Delete domain (soft delete)."""
|
||||
session = Session()
|
||||
try:
|
||||
domain = session.query(Domain).get(domain_id)
|
||||
@@ -192,17 +194,132 @@ def delete_domain(domain_id: int):
|
||||
domain.is_active = False
|
||||
session.commit()
|
||||
|
||||
flash(f'Domain {domain_name} deactivated', 'success')
|
||||
flash(f'Domain {domain_name} disabled', 'success')
|
||||
return redirect(url_for('email.domains_list'))
|
||||
|
||||
except Exception as e:
|
||||
session.rollback()
|
||||
logger.error(f"Error deleting domain: {e}")
|
||||
flash(f'Error deleting domain: {str(e)}', 'error')
|
||||
logger.error(f"Error disabling domain: {e}")
|
||||
flash(f'Error disabling domain: {str(e)}', 'error')
|
||||
return redirect(url_for('email.domains_list'))
|
||||
finally:
|
||||
session.close()
|
||||
|
||||
@email_bp.route('/domains/<int:domain_id>/edit', methods=['GET', 'POST'])
|
||||
def edit_domain(domain_id: int):
|
||||
"""Edit domain."""
|
||||
session = Session()
|
||||
try:
|
||||
domain = session.query(Domain).get(domain_id)
|
||||
if not domain:
|
||||
flash('Domain not found', 'error')
|
||||
return redirect(url_for('email.domains_list'))
|
||||
|
||||
if request.method == 'POST':
|
||||
domain_name = request.form.get('domain_name', '').strip().lower()
|
||||
requires_auth = request.form.get('requires_auth') == 'on'
|
||||
|
||||
if not domain_name:
|
||||
flash('Domain name is required', 'error')
|
||||
return redirect(url_for('email.edit_domain', domain_id=domain_id))
|
||||
|
||||
# Basic domain validation
|
||||
if '.' not in domain_name or len(domain_name.split('.')) < 2:
|
||||
flash('Invalid domain format', 'error')
|
||||
return redirect(url_for('email.edit_domain', domain_id=domain_id))
|
||||
|
||||
# Check if domain name already exists (excluding current domain)
|
||||
existing = session.query(Domain).filter(
|
||||
Domain.domain_name == domain_name,
|
||||
Domain.id != domain_id
|
||||
).first()
|
||||
if existing:
|
||||
flash(f'Domain {domain_name} already exists', 'error')
|
||||
return redirect(url_for('email.edit_domain', domain_id=domain_id))
|
||||
|
||||
old_name = domain.domain_name
|
||||
domain.domain_name = domain_name
|
||||
domain.requires_auth = requires_auth
|
||||
session.commit()
|
||||
|
||||
flash(f'Domain updated from "{old_name}" to "{domain_name}"', 'success')
|
||||
return redirect(url_for('email.domains_list'))
|
||||
|
||||
return render_template('edit_domain.html', domain=domain)
|
||||
|
||||
except Exception as e:
|
||||
session.rollback()
|
||||
logger.error(f"Error editing domain: {e}")
|
||||
flash(f'Error editing domain: {str(e)}', 'error')
|
||||
return redirect(url_for('email.domains_list'))
|
||||
finally:
|
||||
session.close()
|
||||
|
||||
@email_bp.route('/domains/<int:domain_id>/toggle', methods=['POST'])
|
||||
def toggle_domain(domain_id: int):
|
||||
"""Toggle domain active status (Enable/Disable)."""
|
||||
session = Session()
|
||||
try:
|
||||
domain = session.query(Domain).get(domain_id)
|
||||
if not domain:
|
||||
flash('Domain not found', 'error')
|
||||
return redirect(url_for('email.domains_list'))
|
||||
|
||||
old_status = domain.is_active
|
||||
domain.is_active = not old_status
|
||||
session.commit()
|
||||
|
||||
status_text = "enabled" if domain.is_active else "disabled"
|
||||
flash(f'Domain {domain.domain_name} has been {status_text}', 'success')
|
||||
return redirect(url_for('email.domains_list'))
|
||||
|
||||
except Exception as e:
|
||||
session.rollback()
|
||||
logger.error(f"Error toggling domain status: {e}")
|
||||
flash(f'Error toggling domain status: {str(e)}', 'error')
|
||||
return redirect(url_for('email.domains_list'))
|
||||
finally:
|
||||
session.close()
|
||||
|
||||
@email_bp.route('/domains/<int:domain_id>/remove', methods=['POST'])
|
||||
def remove_domain(domain_id: int):
|
||||
"""Permanently remove domain and all associated data."""
|
||||
session = Session()
|
||||
try:
|
||||
domain = session.query(Domain).get(domain_id)
|
||||
if not domain:
|
||||
flash('Domain not found', 'error')
|
||||
return redirect(url_for('email.domains_list'))
|
||||
|
||||
domain_name = domain.domain_name
|
||||
|
||||
# Count associated records
|
||||
user_count = session.query(User).filter_by(domain_id=domain_id).count()
|
||||
ip_count = session.query(WhitelistedIP).filter_by(domain_id=domain_id).count()
|
||||
dkim_count = session.query(DKIMKey).filter_by(domain_id=domain_id).count()
|
||||
|
||||
# Delete associated records
|
||||
session.query(User).filter_by(domain_id=domain_id).delete()
|
||||
session.query(WhitelistedIP).filter_by(domain_id=domain_id).delete()
|
||||
session.query(DKIMKey).filter_by(domain_id=domain_id).delete()
|
||||
session.query(CustomHeader).filter_by(domain_id=domain_id).delete()
|
||||
|
||||
# Delete domain
|
||||
session.delete(domain)
|
||||
session.commit()
|
||||
|
||||
flash(f'Domain {domain_name} and all associated data permanently removed ({user_count} users, {ip_count} IPs, {dkim_count} DKIM keys)', 'success')
|
||||
return redirect(url_for('email.domains_list'))
|
||||
|
||||
except Exception as e:
|
||||
session.rollback()
|
||||
logger.error(f"Error removing domain: {e}")
|
||||
flash(f'Error removing domain: {str(e)}', 'error')
|
||||
return redirect(url_for('email.domains_list'))
|
||||
finally:
|
||||
session.close()
|
||||
|
||||
# User Management Routes
|
||||
@email_bp.route('/users')
|
||||
def users_list():
|
||||
"""List all users."""
|
||||
@@ -266,7 +383,7 @@ def add_user():
|
||||
|
||||
@email_bp.route('/users/<int:user_id>/delete', methods=['POST'])
|
||||
def delete_user(user_id: int):
|
||||
"""Delete user."""
|
||||
"""Disable user (soft delete)."""
|
||||
session = Session()
|
||||
try:
|
||||
user = session.query(User).get(user_id)
|
||||
@@ -278,17 +395,128 @@ def delete_user(user_id: int):
|
||||
user.is_active = False
|
||||
session.commit()
|
||||
|
||||
flash(f'User {user_email} deactivated', 'success')
|
||||
flash(f'User {user_email} disabled', 'success')
|
||||
return redirect(url_for('email.users_list'))
|
||||
|
||||
except Exception as e:
|
||||
session.rollback()
|
||||
logger.error(f"Error deleting user: {e}")
|
||||
flash(f'Error deleting user: {str(e)}', 'error')
|
||||
logger.error(f"Error disabling user: {e}")
|
||||
flash(f'Error disabling user: {str(e)}', 'error')
|
||||
return redirect(url_for('email.users_list'))
|
||||
finally:
|
||||
session.close()
|
||||
|
||||
@email_bp.route('/users/<int:user_id>/enable', methods=['POST'])
|
||||
def enable_user(user_id: int):
|
||||
"""Enable user."""
|
||||
session = Session()
|
||||
try:
|
||||
user = session.query(User).get(user_id)
|
||||
if not user:
|
||||
flash('User not found', 'error')
|
||||
return redirect(url_for('email.users_list'))
|
||||
|
||||
user_email = user.email
|
||||
user.is_active = True
|
||||
session.commit()
|
||||
|
||||
flash(f'User {user_email} enabled', 'success')
|
||||
return redirect(url_for('email.users_list'))
|
||||
|
||||
except Exception as e:
|
||||
session.rollback()
|
||||
logger.error(f"Error enabling user: {e}")
|
||||
flash(f'Error enabling user: {str(e)}', 'error')
|
||||
return redirect(url_for('email.users_list'))
|
||||
finally:
|
||||
session.close()
|
||||
|
||||
@email_bp.route('/users/<int:user_id>/remove', methods=['POST'])
|
||||
def remove_user(user_id: int):
|
||||
"""Permanently remove user."""
|
||||
session = Session()
|
||||
try:
|
||||
user = session.query(User).get(user_id)
|
||||
if not user:
|
||||
flash('User not found', 'error')
|
||||
return redirect(url_for('email.users_list'))
|
||||
|
||||
user_email = user.email
|
||||
session.delete(user)
|
||||
session.commit()
|
||||
|
||||
flash(f'User {user_email} permanently removed', 'success')
|
||||
return redirect(url_for('email.users_list'))
|
||||
|
||||
except Exception as e:
|
||||
session.rollback()
|
||||
logger.error(f"Error removing user: {e}")
|
||||
flash(f'Error removing user: {str(e)}', 'error')
|
||||
return redirect(url_for('email.users_list'))
|
||||
finally:
|
||||
session.close()
|
||||
|
||||
@email_bp.route('/users/<int:user_id>/edit', methods=['GET', 'POST'])
|
||||
def edit_user(user_id: int):
|
||||
"""Edit user."""
|
||||
session = Session()
|
||||
try:
|
||||
user = session.query(User).get(user_id)
|
||||
if not user:
|
||||
flash('User not found', 'error')
|
||||
return redirect(url_for('email.users_list'))
|
||||
|
||||
domains = session.query(Domain).filter_by(is_active=True).order_by(Domain.domain_name).all()
|
||||
|
||||
if request.method == 'POST':
|
||||
email = request.form.get('email', '').strip().lower()
|
||||
password = request.form.get('password', '').strip()
|
||||
domain_id = request.form.get('domain_id', type=int)
|
||||
can_send_as_domain = request.form.get('can_send_as_domain') == 'on'
|
||||
|
||||
if not all([email, domain_id]):
|
||||
flash('Email and domain are required', 'error')
|
||||
return redirect(url_for('email.edit_user', user_id=user_id))
|
||||
|
||||
# Email validation
|
||||
if '@' not in email or '.' not in email.split('@')[1]:
|
||||
flash('Invalid email format', 'error')
|
||||
return redirect(url_for('email.edit_user', user_id=user_id))
|
||||
|
||||
# Check if email already exists (excluding current user)
|
||||
existing = session.query(User).filter(
|
||||
User.email == email,
|
||||
User.id != user_id
|
||||
).first()
|
||||
if existing:
|
||||
flash(f'Email {email} already exists', 'error')
|
||||
return redirect(url_for('email.edit_user', user_id=user_id))
|
||||
|
||||
# Update user
|
||||
user.email = email
|
||||
user.domain_id = domain_id
|
||||
user.can_send_as_domain = can_send_as_domain
|
||||
|
||||
# Update password if provided
|
||||
if password:
|
||||
user.password_hash = hash_password(password)
|
||||
|
||||
session.commit()
|
||||
|
||||
flash(f'User {email} updated successfully', 'success')
|
||||
return redirect(url_for('email.users_list'))
|
||||
|
||||
return render_template('edit_user.html', user=user, domains=domains)
|
||||
|
||||
except Exception as e:
|
||||
session.rollback()
|
||||
logger.error(f"Error editing user: {e}")
|
||||
flash(f'Error editing user: {str(e)}', 'error')
|
||||
return redirect(url_for('email.users_list'))
|
||||
finally:
|
||||
session.close()
|
||||
|
||||
# IP Management Routes
|
||||
@email_bp.route('/ips')
|
||||
def ips_list():
|
||||
"""List all whitelisted IPs."""
|
||||
@@ -350,7 +578,7 @@ def add_ip():
|
||||
|
||||
@email_bp.route('/ips/<int:ip_id>/delete', methods=['POST'])
|
||||
def delete_ip(ip_id: int):
|
||||
"""Delete whitelisted IP."""
|
||||
"""Disable whitelisted IP (soft delete)."""
|
||||
session = Session()
|
||||
try:
|
||||
ip_record = session.query(WhitelistedIP).get(ip_id)
|
||||
@@ -362,17 +590,123 @@ def delete_ip(ip_id: int):
|
||||
ip_record.is_active = False
|
||||
session.commit()
|
||||
|
||||
flash(f'IP {ip_address} removed from whitelist', 'success')
|
||||
flash(f'IP {ip_address} disabled', 'success')
|
||||
return redirect(url_for('email.ips_list'))
|
||||
|
||||
except Exception as e:
|
||||
session.rollback()
|
||||
logger.error(f"Error deleting IP: {e}")
|
||||
flash(f'Error deleting IP: {str(e)}', 'error')
|
||||
logger.error(f"Error disabling IP: {e}")
|
||||
flash(f'Error disabling IP: {str(e)}', 'error')
|
||||
return redirect(url_for('email.ips_list'))
|
||||
finally:
|
||||
session.close()
|
||||
|
||||
@email_bp.route('/ips/<int:ip_id>/enable', methods=['POST'])
|
||||
def enable_ip(ip_id: int):
|
||||
"""Enable whitelisted IP."""
|
||||
session = Session()
|
||||
try:
|
||||
ip_record = session.query(WhitelistedIP).get(ip_id)
|
||||
if not ip_record:
|
||||
flash('IP record not found', 'error')
|
||||
return redirect(url_for('email.ips_list'))
|
||||
|
||||
ip_address = ip_record.ip_address
|
||||
ip_record.is_active = True
|
||||
session.commit()
|
||||
|
||||
flash(f'IP {ip_address} enabled', 'success')
|
||||
return redirect(url_for('email.ips_list'))
|
||||
|
||||
except Exception as e:
|
||||
session.rollback()
|
||||
logger.error(f"Error enabling IP: {e}")
|
||||
flash(f'Error enabling IP: {str(e)}', 'error')
|
||||
return redirect(url_for('email.ips_list'))
|
||||
finally:
|
||||
session.close()
|
||||
|
||||
@email_bp.route('/ips/<int:ip_id>/remove', methods=['POST'])
|
||||
def remove_ip(ip_id: int):
|
||||
"""Permanently remove whitelisted IP."""
|
||||
session = Session()
|
||||
try:
|
||||
ip_record = session.query(WhitelistedIP).get(ip_id)
|
||||
if not ip_record:
|
||||
flash('IP record not found', 'error')
|
||||
return redirect(url_for('email.ips_list'))
|
||||
|
||||
ip_address = ip_record.ip_address
|
||||
session.delete(ip_record)
|
||||
session.commit()
|
||||
|
||||
flash(f'IP {ip_address} permanently removed', 'success')
|
||||
return redirect(url_for('email.ips_list'))
|
||||
|
||||
except Exception as e:
|
||||
session.rollback()
|
||||
logger.error(f"Error removing IP: {e}")
|
||||
flash(f'Error removing IP: {str(e)}', 'error')
|
||||
return redirect(url_for('email.ips_list'))
|
||||
finally:
|
||||
session.close()
|
||||
|
||||
@email_bp.route('/ips/<int:ip_id>/edit', methods=['GET', 'POST'])
|
||||
def edit_ip(ip_id: int):
|
||||
"""Edit whitelisted IP."""
|
||||
session = Session()
|
||||
try:
|
||||
ip_record = session.query(WhitelistedIP).get(ip_id)
|
||||
if not ip_record:
|
||||
flash('IP record not found', 'error')
|
||||
return redirect(url_for('email.ips_list'))
|
||||
|
||||
domains = session.query(Domain).filter_by(is_active=True).order_by(Domain.domain_name).all()
|
||||
|
||||
if request.method == 'POST':
|
||||
ip_address = request.form.get('ip_address', '').strip()
|
||||
domain_id = request.form.get('domain_id', type=int)
|
||||
|
||||
if not all([ip_address, domain_id]):
|
||||
flash('All fields are required', 'error')
|
||||
return redirect(url_for('email.edit_ip', ip_id=ip_id))
|
||||
|
||||
# Basic IP validation
|
||||
try:
|
||||
socket.inet_aton(ip_address)
|
||||
except socket.error:
|
||||
flash('Invalid IP address format', 'error')
|
||||
return redirect(url_for('email.edit_ip', ip_id=ip_id))
|
||||
|
||||
# Check if IP already exists for this domain (excluding current record)
|
||||
existing = session.query(WhitelistedIP).filter(
|
||||
WhitelistedIP.ip_address == ip_address,
|
||||
WhitelistedIP.domain_id == domain_id,
|
||||
WhitelistedIP.id != ip_id
|
||||
).first()
|
||||
if existing:
|
||||
flash(f'IP {ip_address} already whitelisted for this domain', 'error')
|
||||
return redirect(url_for('email.edit_ip', ip_id=ip_id))
|
||||
|
||||
# Update IP record
|
||||
ip_record.ip_address = ip_address
|
||||
ip_record.domain_id = domain_id
|
||||
session.commit()
|
||||
|
||||
flash(f'IP whitelist record updated', 'success')
|
||||
return redirect(url_for('email.ips_list'))
|
||||
|
||||
return render_template('edit_ip.html', ip_record=ip_record, domains=domains)
|
||||
|
||||
except Exception as e:
|
||||
session.rollback()
|
||||
logger.error(f"Error editing IP: {e}")
|
||||
flash(f'Error editing IP: {str(e)}', 'error')
|
||||
return redirect(url_for('email.ips_list'))
|
||||
finally:
|
||||
session.close()
|
||||
|
||||
# DKIM Management Routes
|
||||
@email_bp.route('/dkim')
|
||||
def dkim_list():
|
||||
"""List all DKIM keys and DNS records."""
|
||||
@@ -449,20 +783,9 @@ def regenerate_dkim(domain_id: int):
|
||||
finally:
|
||||
session.close()
|
||||
|
||||
@email_bp.route('/dkim/<int:dkim_id>/update_selector', methods=['POST'])
|
||||
def update_dkim_selector(dkim_id: int):
|
||||
"""Update DKIM selector name."""
|
||||
new_selector = request.form.get('selector', '').strip()
|
||||
|
||||
if not new_selector:
|
||||
flash('Selector name is required', 'error')
|
||||
return redirect(url_for('email.dkim_list'))
|
||||
|
||||
# Validate selector (alphanumeric only)
|
||||
if not re.match(r'^[a-zA-Z0-9]+$', new_selector):
|
||||
flash('Selector must contain only letters and numbers', 'error')
|
||||
return redirect(url_for('email.dkim_list'))
|
||||
|
||||
@email_bp.route('/dkim/<int:dkim_id>/edit', methods=['GET', 'POST'])
|
||||
def edit_dkim(dkim_id: int):
|
||||
"""Edit DKIM key selector."""
|
||||
session = Session()
|
||||
try:
|
||||
dkim_key = session.query(DKIMKey).get(dkim_id)
|
||||
@@ -470,21 +793,103 @@ def update_dkim_selector(dkim_id: int):
|
||||
flash('DKIM key not found', 'error')
|
||||
return redirect(url_for('email.dkim_list'))
|
||||
|
||||
old_selector = dkim_key.selector
|
||||
dkim_key.selector = new_selector
|
||||
session.commit()
|
||||
domain = session.query(Domain).get(dkim_key.domain_id)
|
||||
|
||||
flash(f'DKIM selector updated from {old_selector} to {new_selector}', 'success')
|
||||
return redirect(url_for('email.dkim_list'))
|
||||
if request.method == 'POST':
|
||||
new_selector = request.form.get('selector', '').strip()
|
||||
|
||||
if not new_selector:
|
||||
flash('Selector name is required', 'error')
|
||||
return render_template('edit_dkim.html', dkim_key=dkim_key, domain=domain)
|
||||
|
||||
# Validate selector (alphanumeric only)
|
||||
if not re.match(r'^[a-zA-Z0-9_-]+$', new_selector):
|
||||
flash('Selector must contain only letters, numbers, hyphens, and underscores', 'error')
|
||||
return render_template('edit_dkim.html', dkim_key=dkim_key, domain=domain)
|
||||
|
||||
# Check for duplicate selector in same domain
|
||||
existing = session.query(DKIMKey).filter_by(
|
||||
domain_id=dkim_key.domain_id,
|
||||
selector=new_selector,
|
||||
is_active=True
|
||||
).filter(DKIMKey.id != dkim_id).first()
|
||||
|
||||
if existing:
|
||||
flash(f'A DKIM key with selector "{new_selector}" already exists for this domain', 'error')
|
||||
return render_template('edit_dkim.html', dkim_key=dkim_key, domain=domain)
|
||||
|
||||
old_selector = dkim_key.selector
|
||||
dkim_key.selector = new_selector
|
||||
session.commit()
|
||||
|
||||
flash(f'DKIM selector updated from "{old_selector}" to "{new_selector}" for {domain.domain_name}', 'success')
|
||||
return redirect(url_for('email.dkim_list'))
|
||||
|
||||
return render_template('edit_dkim.html', dkim_key=dkim_key, domain=domain)
|
||||
|
||||
except Exception as e:
|
||||
session.rollback()
|
||||
logger.error(f"Error updating DKIM selector: {e}")
|
||||
flash(f'Error updating DKIM selector: {str(e)}', 'error')
|
||||
logger.error(f"Error editing DKIM: {e}")
|
||||
flash(f'Error editing DKIM key: {str(e)}', 'error')
|
||||
return redirect(url_for('email.dkim_list'))
|
||||
finally:
|
||||
session.close()
|
||||
|
||||
@email_bp.route('/dkim/<int:dkim_id>/toggle', methods=['POST'])
|
||||
def toggle_dkim(dkim_id: int):
|
||||
"""Toggle DKIM key active status (Enable/Disable)."""
|
||||
session = Session()
|
||||
try:
|
||||
dkim_key = session.query(DKIMKey).get(dkim_id)
|
||||
if not dkim_key:
|
||||
flash('DKIM key not found', 'error')
|
||||
return redirect(url_for('email.dkim_list'))
|
||||
|
||||
domain = session.query(Domain).get(dkim_key.domain_id)
|
||||
old_status = dkim_key.is_active
|
||||
dkim_key.is_active = not old_status
|
||||
session.commit()
|
||||
|
||||
status_text = "enabled" if dkim_key.is_active else "disabled"
|
||||
flash(f'DKIM key for {domain.domain_name} (selector: {dkim_key.selector}) has been {status_text}', 'success')
|
||||
return redirect(url_for('email.dkim_list'))
|
||||
|
||||
except Exception as e:
|
||||
session.rollback()
|
||||
logger.error(f"Error toggling DKIM status: {e}")
|
||||
flash(f'Error toggling DKIM status: {str(e)}', 'error')
|
||||
return redirect(url_for('email.dkim_list'))
|
||||
finally:
|
||||
session.close()
|
||||
|
||||
@email_bp.route('/dkim/<int:dkim_id>/remove', methods=['POST'])
|
||||
def remove_dkim(dkim_id: int):
|
||||
"""Permanently remove DKIM key."""
|
||||
session = Session()
|
||||
try:
|
||||
dkim_key = session.query(DKIMKey).get(dkim_id)
|
||||
if not dkim_key:
|
||||
flash('DKIM key not found', 'error')
|
||||
return redirect(url_for('email.dkim_list'))
|
||||
|
||||
domain = session.query(Domain).get(dkim_key.domain_id)
|
||||
selector = dkim_key.selector
|
||||
|
||||
session.delete(dkim_key)
|
||||
session.commit()
|
||||
|
||||
flash(f'DKIM key for {domain.domain_name} (selector: {selector}) has been permanently removed', 'success')
|
||||
return redirect(url_for('email.dkim_list'))
|
||||
|
||||
except Exception as e:
|
||||
session.rollback()
|
||||
logger.error(f"Error removing DKIM key: {e}")
|
||||
flash(f'Error removing DKIM key: {str(e)}', 'error')
|
||||
return redirect(url_for('email.dkim_list'))
|
||||
finally:
|
||||
session.close()
|
||||
|
||||
# AJAX DNS Check Routes
|
||||
@email_bp.route('/dkim/check_dns', methods=['POST'])
|
||||
def check_dkim_dns():
|
||||
"""Check DKIM DNS record via AJAX."""
|
||||
@@ -527,6 +932,7 @@ def check_spf_dns():
|
||||
|
||||
return jsonify(result)
|
||||
|
||||
# Settings Routes
|
||||
@email_bp.route('/settings')
|
||||
def settings():
|
||||
"""Display and edit server settings."""
|
||||
@@ -560,6 +966,7 @@ def update_settings():
|
||||
flash(f'Error updating settings: {str(e)}', 'error')
|
||||
return redirect(url_for('email.settings'))
|
||||
|
||||
# Logs Routes
|
||||
@email_bp.route('/logs')
|
||||
def logs():
|
||||
"""Display email and authentication logs."""
|
||||
@@ -629,7 +1036,6 @@ def logs():
|
||||
@email_bp.errorhandler(404)
|
||||
def not_found(error):
|
||||
"""Handle 404 errors."""
|
||||
from datetime import datetime
|
||||
return render_template('error.html',
|
||||
error_code=404,
|
||||
error_message='Page not found',
|
||||
@@ -638,7 +1044,6 @@ def not_found(error):
|
||||
@email_bp.errorhandler(500)
|
||||
def internal_error(error):
|
||||
"""Handle 500 errors."""
|
||||
from datetime import datetime
|
||||
logger.error(f"Internal error: {error}")
|
||||
return render_template('error.html',
|
||||
error_code=500,
|
||||
@@ -6,6 +6,7 @@
|
||||
<style>
|
||||
.dns-record {
|
||||
font-family: 'Courier New', monospace;
|
||||
color: black;
|
||||
background-color: var(--bs-gray-100);
|
||||
border-radius: 0.375rem;
|
||||
padding: 0.75rem;
|
||||
@@ -42,7 +43,7 @@
|
||||
|
||||
{% for item in dkim_data %}
|
||||
<div class="card mb-4" id="domain-{{ item.domain.id }}">
|
||||
<div class="card-header">
|
||||
<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>
|
||||
@@ -58,6 +59,34 @@
|
||||
<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"
|
||||
@@ -67,9 +96,13 @@
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
<div class="ms-auto">
|
||||
<i class="bi bi-chevron-down" id="chevron-{{ item.domain.id }}"></i>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<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">
|
||||
@@ -160,6 +193,7 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
@@ -284,8 +318,9 @@
|
||||
const domains = document.querySelectorAll('[id^="domain-"]');
|
||||
for (const domainCard of domains) {
|
||||
const domainId = domainCard.id.split('-')[1];
|
||||
// Extract domain name and selector from the card
|
||||
const domainName = domainCard.querySelector('h5').textContent.trim().split('\n')[0].trim();
|
||||
// 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;
|
||||
@@ -314,5 +349,29 @@
|
||||
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 %}
|
||||
@@ -79,21 +79,41 @@
|
||||
</td>
|
||||
<td>
|
||||
<div class="btn-group btn-group-sm" role="group">
|
||||
<a href="{{ url_for('email.dkim_list') }}#domain-{{ domain.id }}"
|
||||
class="btn btn-outline-info"
|
||||
title="Manage DKIM">
|
||||
<i class="bi bi-key"></i>
|
||||
<!-- 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>
|
||||
{% if domain.is_active %}
|
||||
<form method="post" action="{{ url_for('email.delete_domain', domain_id=domain.id) }}" class="d-inline">
|
||||
|
||||
<!-- 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-danger"
|
||||
data-confirm="Are you sure you want to deactivate domain '{{ domain.domain_name }}'?"
|
||||
title="Deactivate Domain">
|
||||
<i class="bi bi-trash"></i>
|
||||
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>
|
||||
</form>
|
||||
{% endif %}
|
||||
{% 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>
|
||||
129
email_server/server_web_ui/templates/edit_dkim.html
Normal file
129
email_server/server_web_ui/templates/edit_dkim.html
Normal 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 %}
|
||||
164
email_server/server_web_ui/templates/edit_domain.html
Normal file
164
email_server/server_web_ui/templates/edit_domain.html
Normal 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 %}
|
||||
163
email_server/server_web_ui/templates/edit_ip.html
Normal file
163
email_server/server_web_ui/templates/edit_ip.html
Normal 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 %}
|
||||
190
email_server/server_web_ui/templates/edit_user.html
Normal file
190
email_server/server_web_ui/templates/edit_user.html
Normal 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 %}
|
||||
@@ -61,7 +61,7 @@
|
||||
<div class="card-footer">
|
||||
<div class="d-flex justify-content-between align-items-center">
|
||||
<div>
|
||||
<a href="{{ url_for('email_management.dashboard') }}" class="btn btn-primary">
|
||||
<a href="{{ url_for('email.dashboard') }}" class="btn btn-primary">
|
||||
<i class="fas fa-home me-1"></i>
|
||||
Return to Dashboard
|
||||
</a>
|
||||
@@ -65,19 +65,45 @@
|
||||
</small>
|
||||
</td>
|
||||
<td>
|
||||
{% if ip.is_active %}
|
||||
<form method="post" action="{{ url_for('email.delete_ip', ip_id=ip.id) }}" class="d-inline">
|
||||
<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"
|
||||
onclick="return confirm('Remove {{ ip.ip_address }} from whitelist?')">
|
||||
title="Permanently Remove IP"
|
||||
onclick="return confirm('Permanently remove {{ ip.ip_address }}? This cannot be undone!')">
|
||||
<i class="bi bi-trash"></i>
|
||||
</button>
|
||||
</form>
|
||||
{% else %}
|
||||
<span class="text-muted">
|
||||
<i class="bi bi-dash"></i>
|
||||
</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
@@ -123,7 +149,7 @@
|
||||
<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.strftime('%Y-%m-%d') if latest else 'N/A' }}
|
||||
{{ latest.created_at.strftime('%Y-%m-%d') if latest else 'N/A' }}
|
||||
</li>
|
||||
</ul>
|
||||
{% endif %}
|
||||
@@ -81,20 +81,45 @@
|
||||
</small>
|
||||
</td>
|
||||
<td>
|
||||
{% if user.is_active %}
|
||||
<form method="post" action="{{ url_for('email.delete_user', user_id=user.id) }}" class="d-inline">
|
||||
<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"
|
||||
data-confirm="Are you sure you want to deactivate user '{{ user.email }}'?"
|
||||
title="Deactivate User">
|
||||
title="Permanently Remove User"
|
||||
onclick="return confirm('Permanently remove user {{ user.email }}? This cannot be undone!')">
|
||||
<i class="bi bi-trash"></i>
|
||||
</button>
|
||||
</form>
|
||||
{% else %}
|
||||
<span class="text-muted">
|
||||
<i class="bi bi-dash-circle"></i>
|
||||
</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
3
main.py
3
main.py
@@ -1,3 +1,6 @@
|
||||
"""
|
||||
Starts the email server, no web UI!
|
||||
"""
|
||||
from email_server.server_runner import start_server
|
||||
from email_server.tool_box import get_logger
|
||||
import asyncio
|
||||
|
||||
@@ -19,7 +19,6 @@ Flask-SQLAlchemy
|
||||
Jinja2
|
||||
Werkzeug
|
||||
requests
|
||||
waitress
|
||||
|
||||
# Additional utilities
|
||||
alembic
|
||||
Reference in New Issue
Block a user