diff --git a/app.py b/app.py
index 6553250..d8c448d 100644
--- a/app.py
+++ b/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({
diff --git a/email_frontend/example_app.py b/email_frontend/example_app.py
deleted file mode 100644
index 8dc2a26..0000000
--- a/email_frontend/example_app.py
+++ /dev/null
@@ -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()
diff --git a/email_frontend/templates/dkim.html b/email_frontend/templates/dkim.html
deleted file mode 100644
index d5bc82e..0000000
--- a/email_frontend/templates/dkim.html
+++ /dev/null
@@ -1,318 +0,0 @@
-{% extends "base.html" %}
-
-{% block title %}DKIM Keys - Email Server{% endblock %}
-
-{% block extra_css %}
-
-{% endblock %}
-
-{% block content %}
-
-
-
-
- DKIM Key Management
-
-
-
-
- Check All DNS
-
-
-
-
- {% for item in dkim_data %}
-
-
-
-
-
-
-
-
- DKIM DNS Record
-
-
- Not checked
-
-
-
-
Name:
-
{{ item.dns_record.name }}
-
-
- Type: TXT
-
-
-
Value:
-
{{ item.dns_record.value }}
-
-
-
- Copy Value
-
-
-
-
-
-
-
- SPF DNS Record
-
-
- Not checked
-
-
-
-
Name:
-
{{ item.domain.domain_name }}
-
-
- Type: TXT
-
- {% if item.existing_spf %}
-
-
Current SPF:
-
{{ item.existing_spf }}
-
- {% endif %}
-
-
Recommended SPF:
-
{{ item.recommended_spf }}
-
-
-
- Copy SPF
-
-
-
-
-
-
-
-
Key Information
-
-
- Selector:
- {{ item.dkim_key.selector }}
-
-
- Created:
- {{ item.dkim_key.created_at.strftime('%Y-%m-%d %H:%M') }}
-
-
- Server IP:
- {{ item.public_ip }}
-
-
- Status:
- {% if item.dkim_key.is_active %}
- Active
- {% else %}
- Inactive
- {% endif %}
-
-
-
-
-
-
- {% endfor %}
-
- {% if not dkim_data %}
-
-
-
-
No DKIM Keys Found
-
Add domains first to automatically generate DKIM keys
-
-
- Add Domain
-
-
-
- {% endif %}
-
-
-
-
-{% endblock %}
-
-{% block extra_js %}
-
-{% endblock %}
diff --git a/email_frontend/templates/domains.html b/email_frontend/templates/domains.html
deleted file mode 100644
index 96d22ef..0000000
--- a/email_frontend/templates/domains.html
+++ /dev/null
@@ -1,174 +0,0 @@
-{% extends "base.html" %}
-
-{% block title %}Domains - Email Server Management{% endblock %}
-{% block page_title %}Domain Management{% endblock %}
-
-{% block content %}
-
-
-
- Domains
-
-
-
- Add Domain
-
-
-
-
-
-
- {% if domains %}
-
-
-
-
- Domain Name
- Status
- Created
- Users
- DKIM
- Actions
-
-
-
- {% for domain in domains %}
-
-
- {{ domain.domain_name }}
-
-
- {% if domain.is_active %}
-
-
- Active
-
- {% else %}
-
-
- Inactive
-
- {% endif %}
-
-
-
- {{ domain.created_at.strftime('%Y-%m-%d %H:%M') }}
-
-
-
-
- {{ domain.users|length if domain.users else 0 }} users
-
-
-
- {% set has_dkim = domain.dkim_keys and domain.dkim_keys|selectattr('is_active')|list %}
- {% if has_dkim %}
-
-
-
- {% else %}
-
-
-
- {% endif %}
-
-
-
-
-
-
- {% if domain.is_active %}
-
- {% endif %}
-
-
-
- {% endfor %}
-
-
-
- {% else %}
-
-
-
No domains configured
-
Get started by adding your first domain
-
-
- Add Your First Domain
-
-
- {% endif %}
-
-
-
-{% if domains %}
-
-
-
-
-
-
-
-
- Active domains: {{ domains|selectattr('is_active')|list|length }}
-
-
-
- DKIM configured: {{ domains|selectattr('dkim_keys')|list|length }}
-
-
-
- Total users: {{ domains|sum(attribute='users')|length if domains[0].users is defined else 'N/A' }}
-
-
-
-
-
-
-
-
-
-
-
-
- DKIM keys are automatically generated for new domains
-
-
-
- Configure DNS records after adding domains
-
-
-
- Add users or whitelist IPs for authentication
-
-
-
-
-
-
-{% endif %}
-{% endblock %}
diff --git a/email_frontend/templates/email/add_domain.html b/email_frontend/templates/email/add_domain.html
deleted file mode 100644
index 65e3a21..0000000
--- a/email_frontend/templates/email/add_domain.html
+++ /dev/null
@@ -1,112 +0,0 @@
-{% extends "base.html" %}
-
-{% block title %}Add Domain - Email Server Management{% endblock %}
-{% block page_title %}Add New Domain{% endblock %}
-
-{% block content %}
-
-
-
-
-
-
-
-
-
-
-
- Valid Examples
-
-
- example.com
- mail.example.com
- my-domain.org
- company.co.uk
-
-
-
-
-
- Invalid Examples
-
-
- http://example.com
- example
- .example.com
- example..com
-
-
-
-
-
-
-
-{% endblock %}
-
-{% block extra_js %}
-
-{% endblock %}
diff --git a/email_frontend/templates/email/add_ip.html b/email_frontend/templates/email/add_ip.html
deleted file mode 100644
index f49d80f..0000000
--- a/email_frontend/templates/email/add_ip.html
+++ /dev/null
@@ -1,228 +0,0 @@
-{% extends "base.html" %}
-
-{% block title %}Add IP Address - Email Server{% endblock %}
-
-{% block content %}
-
-
-
-
-
-
-
-
-
-
- Detecting...
-
-
-
- Use This IP
-
-
-
-
-
- {% if domains %}
-
-
-
-
- {% for domain in domains %}
-
-
{{ domain.domain_name }}
-
- Created: {{ domain.created_at.strftime('%Y-%m-%d') }}
-
-
- {% if not loop.last %}
{% endif %}
- {% endfor %}
-
-
-
- {% endif %}
-
-
-
-
-
-
-
-
-
-
-
-
- Application Servers
-
-
- Web applications that need to send transactional emails
- (password resets, notifications, etc.)
-
-
-
-
-
- Scheduled Tasks
-
-
- Cron jobs or scheduled scripts that send automated
- reports or alerts
-
-
-
-
-
-
-
- Monitoring Systems
-
-
- Monitoring tools that send alerts and status updates
- to administrators
-
-
-
-
-
- Cloud Services
-
-
- Cloud-based applications or services that need to
- send emails on behalf of your domain
-
-
-
-
-
-
-
-{% endblock %}
-
-{% block extra_js %}
-
-{% endblock %}
diff --git a/email_frontend/templates/email/add_user.html b/email_frontend/templates/email/add_user.html
deleted file mode 100644
index 902a042..0000000
--- a/email_frontend/templates/email/add_user.html
+++ /dev/null
@@ -1,176 +0,0 @@
-{% extends "base.html" %}
-
-{% block title %}Add User - Email Server{% endblock %}
-
-{% block content %}
-
-
-
-
- {% if domains %}
-
-
-
-
-
- {% for domain in domains %}
-
-
-
{{ domain.domain_name }}
-
- Created: {{ domain.created_at.strftime('%Y-%m-%d') }}
-
-
-
- {% endfor %}
-
-
-
-
- {% endif %}
-
-{% endblock %}
-
-{% block extra_js %}
-
-{% endblock %}
diff --git a/email_frontend/templates/email/dashboard.html b/email_frontend/templates/email/dashboard.html
deleted file mode 100644
index 60382f6..0000000
--- a/email_frontend/templates/email/dashboard.html
+++ /dev/null
@@ -1,285 +0,0 @@
-{% extends "base.html" %}
-
-{% block title %}Dashboard - Email Server Management{% endblock %}
-{% block page_title %}Dashboard{% endblock %}
-
-{% block content %}
-
-
-
-
-
-
-
-
-
- Domains
-
- {{ domain_count }}
- Active domains
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- Users
-
- {{ user_count }}
- Authenticated users
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- DKIM Keys
-
- {{ dkim_count }}
- Active DKIM keys
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- Status
-
-
-
- Online
-
- Server running
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- {% if recent_emails %}
-
-
-
-
- Time
- From
- To
- Status
- DKIM
-
-
-
- {% for email in recent_emails %}
-
-
-
- {{ email.created_at.strftime('%H:%M:%S') }}
-
-
-
-
- {{ email.mail_from }}
-
-
-
-
- {{ email.to_address }}
-
-
-
- {% if email.status == 'relayed' %}
-
-
- Sent
-
- {% else %}
-
-
- Failed
-
- {% endif %}
-
-
- {% if email.dkim_signed %}
-
-
-
- {% else %}
-
-
-
- {% endif %}
-
-
- {% endfor %}
-
-
-
- {% else %}
-
-
-
No email activity yet
-
- {% endif %}
-
-
-
-
-
-
-
-
-
- {% if recent_auths %}
-
- {% for auth in recent_auths %}
-
-
-
- {% if auth.success %}
-
- {% else %}
-
- {% endif %}
- {{ auth.auth_type|title }}
-
-
- {{ auth.identifier }}
-
-
-
- {{ auth.timestamp.strftime('%H:%M:%S') }}
-
-
-
- {{ auth.ip_address }}
-
-
- {% endfor %}
-
- {% else %}
-
-
-
No authentication activity yet
-
- {% endif %}
-
-
-
-
-
-
-
-{% endblock %}
-
-{% block extra_js %}
-
-{% endblock %}
diff --git a/email_frontend/templates/email/error.html b/email_frontend/templates/email/error.html
deleted file mode 100644
index 09cf2e9..0000000
--- a/email_frontend/templates/email/error.html
+++ /dev/null
@@ -1,173 +0,0 @@
-{% extends "base.html" %}
-
-{% block title %}Error - SMTP Management{% endblock %}
-
-{% block content %}
-
-
-
-
-
- {% if error_code %}
-
-
Error Code:
-
- {{ error_code }}
-
-
- {% endif %}
-
- {% if error_message %}
-
-
Message:
-
-
- {{ error_message }}
-
-
-
- {% endif %}
-
- {% if error_details %}
-
- {% endif %}
-
-
-
Timestamp:
-
- {{ moment().format('YYYY-MM-DD HH:mm:ss') }}
-
-
-
-
-
Request URL:
-
- {{ request.url if request else 'Unknown' }}
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
Database Issues:
-
- Check database connection settings
- Verify database tables exist
- Check database permissions
-
-
-
-
Configuration Issues:
-
- Verify settings.ini file exists
- Check file permissions
- Validate configuration values
-
-
-
-
-
-
Network Issues:
-
- Check firewall settings
- Verify DNS resolution
- Test network connectivity
-
-
-
-
Permission Issues:
-
- Check file system permissions
- Verify user authentication
- Review access controls
-
-
-
-
-
-
-
-
-
-{% endblock %}
diff --git a/email_frontend/templates/email/ips.html b/email_frontend/templates/email/ips.html
deleted file mode 100644
index 5653e72..0000000
--- a/email_frontend/templates/email/ips.html
+++ /dev/null
@@ -1,203 +0,0 @@
-{% extends "base.html" %}
-
-{% block title %}Whitelisted IPs - Email Server{% endblock %}
-
-{% block content %}
-
-
-
-
- Whitelisted IP Addresses
-
-
-
- Add IP Address
-
-
-
-
-
-
-
-
- {% if ips %}
-
-
-
-
- IP Address
- Domain
- Status
- Added
- Actions
-
-
-
- {% for ip, domain in ips %}
-
-
- {{ ip.ip_address }}
-
-
- {{ domain.domain_name }}
-
-
- {% if ip.is_active %}
-
-
- Active
-
- {% else %}
-
-
- Inactive
-
- {% endif %}
-
-
-
- {{ ip.created_at.strftime('%Y-%m-%d %H:%M') }}
-
-
-
- {% if ip.is_active %}
-
- {% else %}
-
-
-
- {% endif %}
-
-
- {% endfor %}
-
-
-
- {% else %}
-
-
-
No IP Addresses Whitelisted
-
Add IP addresses to allow authentication without username/password
-
-
- Add First IP Address
-
-
- {% endif %}
-
-
-
-
-
-
-
-
-
- {% if ips %}
-
-
-
- Active IPs: {{ ips|selectattr('0.is_active')|list|length }}
-
-
-
- Domains covered: {{ ips|map(attribute='1.domain_name')|unique|list|length }}
-
-
-
- Latest addition:
- {% set latest = ips|map(attribute='0')|max(attribute='created_at') %}
- {{ latest.strftime('%Y-%m-%d') if latest else 'N/A' }}
-
-
- {% endif %}
-
-
-
-
- How IP Whitelisting Works
-
-
- Whitelisted IPs can send emails without username/password authentication
- Each IP is associated with a specific domain
- IP can only send emails for its authorized domain
- Useful for server-to-server email sending
-
-
-
-
-
-
-
-
-
-
-
-
- Detecting...
-
-
-
- Add This IP
-
-
-
-
-
-
-
-{% endblock %}
-
-{% block extra_js %}
-
-{% endblock %}
diff --git a/email_frontend/templates/email/users.html b/email_frontend/templates/email/users.html
deleted file mode 100644
index a64ceae..0000000
--- a/email_frontend/templates/email/users.html
+++ /dev/null
@@ -1,170 +0,0 @@
-{% extends "base.html" %}
-
-{% block title %}Users - Email Server Management{% endblock %}
-{% block page_title %}User Management{% endblock %}
-
-{% block content %}
-
-
-
- Users
-
-
-
- Add User
-
-
-
-
-
-
- {% if users %}
-
-
-
-
- Email
- Domain
- Permissions
- Status
- Created
- Actions
-
-
-
- {% for user, domain in users %}
-
-
- {{ user.email }}
-
-
- {{ domain.domain_name }}
-
-
- {% if user.can_send_as_domain %}
-
-
- Domain Admin
-
-
- Can send as *@{{ domain.domain_name }}
- {% else %}
-
-
- User
-
-
- Can only send as {{ user.email }}
- {% endif %}
-
-
- {% if user.is_active %}
-
-
- Active
-
- {% else %}
-
-
- Inactive
-
- {% endif %}
-
-
-
- {{ user.created_at.strftime('%Y-%m-%d %H:%M') }}
-
-
-
- {% if user.is_active %}
-
- {% else %}
-
-
-
- {% endif %}
-
-
- {% endfor %}
-
-
-
- {% else %}
-
-
-
No users configured
-
Add users to enable username/password authentication
-
-
- Add Your First User
-
-
- {% endif %}
-
-
-
-{% if users %}
-
-
-
-
-
-
-
-
- Active users: {{ users|selectattr('0.is_active')|list|length }}
-
-
-
- Domain admins: {{ users|selectattr('0.can_send_as_domain')|list|length }}
-
-
-
- Regular users: {{ users|rejectattr('0.can_send_as_domain')|list|length }}
-
-
-
-
-
-
-
-
-
-
-
- Domain Admin
- Can send as any email address in their domain
-
-
- Regular User
- Can only send as their own email address
-
-
-
-
-
-
-{% endif %}
-{% endblock %}
diff --git a/email_frontend/templates/logs.html b/email_frontend/templates/logs.html
deleted file mode 100644
index 6d9f7fd..0000000
--- a/email_frontend/templates/logs.html
+++ /dev/null
@@ -1,322 +0,0 @@
-{% extends "base.html" %}
-
-{% block title %}Logs - Email Server{% endblock %}
-
-{% block extra_css %}
-
-{% endblock %}
-
-{% block content %}
-
-
-
-
-
-
-
-
- {% if logs %}
- {% if filter_type == 'all' %}
-
- {% for log_entry in logs %}
- {% if log_entry.type == 'email' %}
- {% set log = log_entry.data %}
-
-
-
- EMAIL
- {{ log.mail_from }} → {{ log.rcpt_tos }}
- {% if log.dkim_signed %}
-
-
- DKIM
-
- {% endif %}
-
-
{{ log.created_at.strftime('%Y-%m-%d %H:%M:%S') }}
-
-
-
- Status:
- {% if log.status == 'relayed' %}
- Sent Successfully
- {% else %}
- Failed
- {% endif %}
-
-
- Message ID: {{ log.message_id }}
-
-
- {% if log.subject %}
-
- Subject: {{ log.subject }}
-
- {% endif %}
-
- {% else %}
- {% set log = log_entry.data %}
-
-
-
- AUTH
- {{ log.identifier }}
-
- {{ 'Success' if log.success else 'Failed' }}
-
-
-
{{ log.created_at.strftime('%Y-%m-%d %H:%M:%S') }}
-
-
-
- Type: {{ log.auth_type.upper() }}
-
-
- IP: {{ log.ip_address or 'N/A' }}
-
-
- {% if log.message %}
-
- Message: {{ log.message }}
-
- {% endif %}
-
- {% endif %}
- {% endfor %}
- {% elif filter_type == 'emails' %}
-
- {% for log in logs %}
-
-
-
- {{ log.mail_from }} → {{ log.rcpt_tos }}
- {% if log.dkim_signed %}
-
-
- DKIM
-
- {% endif %}
-
-
{{ log.created_at.strftime('%Y-%m-%d %H:%M:%S') }}
-
-
-
- Status:
- {% if log.status == 'relayed' %}
- Sent
- {% else %}
- Failed
- {% endif %}
-
-
- Peer: {{ log.peer }}
-
-
- Message ID: {{ log.message_id }}
-
-
- {% if log.subject %}
-
- Subject: {{ log.subject }}
-
- {% endif %}
- {% if log.content and log.content|length > 50 %}
-
- {% endif %}
-
- {% endfor %}
- {% else %}
-
- {% for log in logs %}
-
-
-
- {{ log.identifier }}
-
- {{ 'Success' if log.success else 'Failed' }}
-
-
-
{{ log.created_at.strftime('%Y-%m-%d %H:%M:%S') }}
-
-
-
- Type: {{ log.auth_type.upper() }}
-
-
- IP: {{ log.ip_address or 'N/A' }}
-
-
- Result:
- {% if log.success %}
- Authenticated
- {% else %}
- Rejected
- {% endif %}
-
-
- {% if log.message %}
-
- Details: {{ log.message }}
-
- {% endif %}
-
- {% endfor %}
- {% endif %}
-
-
- {% if has_prev or has_next %}
-
-
-
- {% endif %}
- {% else %}
-
-
-
No Logs Found
-
- {% 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 %}
-
-
- {% endif %}
-
-
-
-
-
-{% endblock %}
-
-{% block extra_js %}
-
-{% endblock %}
diff --git a/email_frontend/templates/settings.html b/email_frontend/templates/settings.html
deleted file mode 100644
index f3286ea..0000000
--- a/email_frontend/templates/settings.html
+++ /dev/null
@@ -1,355 +0,0 @@
-{% extends "base.html" %}
-
-{% block title %}Server Settings - Email Server{% endblock %}
-
-{% block extra_css %}
-
-{% endblock %}
-
-{% block content %}
-
-
-
-
- Server Settings
-
-
-
-
- Reset to Defaults
-
-
-
- Export Config
-
-
-
-
-
-
-
-
-
-
-
-
-
-
Are you sure you want to reset all settings to their default values?
-
Warning: This action cannot be undone.
-
-
-
-
-
-{% endblock %}
-
-{% block extra_js %}
-
-{% endblock %}
diff --git a/email_frontend/README.md b/email_server/server_web_ui/README.md
similarity index 100%
rename from email_frontend/README.md
rename to email_server/server_web_ui/README.md
diff --git a/email_frontend/__init__.py b/email_server/server_web_ui/__init__.py
similarity index 100%
rename from email_frontend/__init__.py
rename to email_server/server_web_ui/__init__.py
diff --git a/email_frontend/requirements.txt b/email_server/server_web_ui/requirements.txt
similarity index 100%
rename from email_frontend/requirements.txt
rename to email_server/server_web_ui/requirements.txt
diff --git a/email_frontend/blueprint.py b/email_server/server_web_ui/routes.py
similarity index 56%
rename from email_frontend/blueprint.py
rename to email_server/server_web_ui/routes.py
index 37c8fa1..24da34a 100644
--- a/email_frontend/blueprint.py
+++ b/email_server/server_web_ui/routes.py
@@ -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//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//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//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//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//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//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//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//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//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//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//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//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//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//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//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//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,
diff --git a/email_frontend/static/css/smtp-management.css b/email_server/server_web_ui/static/css/smtp-management.css
similarity index 100%
rename from email_frontend/static/css/smtp-management.css
rename to email_server/server_web_ui/static/css/smtp-management.css
diff --git a/email_frontend/static/js/smtp-management.js b/email_server/server_web_ui/static/js/smtp-management.js
similarity index 100%
rename from email_frontend/static/js/smtp-management.js
rename to email_server/server_web_ui/static/js/smtp-management.js
diff --git a/email_frontend/templates/add_domain.html b/email_server/server_web_ui/templates/add_domain.html
similarity index 100%
rename from email_frontend/templates/add_domain.html
rename to email_server/server_web_ui/templates/add_domain.html
diff --git a/email_frontend/templates/add_ip.html b/email_server/server_web_ui/templates/add_ip.html
similarity index 100%
rename from email_frontend/templates/add_ip.html
rename to email_server/server_web_ui/templates/add_ip.html
diff --git a/email_frontend/templates/add_user.html b/email_server/server_web_ui/templates/add_user.html
similarity index 100%
rename from email_frontend/templates/add_user.html
rename to email_server/server_web_ui/templates/add_user.html
diff --git a/email_frontend/templates/base.html b/email_server/server_web_ui/templates/base.html
similarity index 100%
rename from email_frontend/templates/base.html
rename to email_server/server_web_ui/templates/base.html
diff --git a/email_frontend/templates/dashboard.html b/email_server/server_web_ui/templates/dashboard.html
similarity index 100%
rename from email_frontend/templates/dashboard.html
rename to email_server/server_web_ui/templates/dashboard.html
diff --git a/email_frontend/templates/email/dkim.html b/email_server/server_web_ui/templates/dkim.html
similarity index 78%
rename from email_frontend/templates/email/dkim.html
rename to email_server/server_web_ui/templates/dkim.html
index d5bc82e..2787a00 100644
--- a/email_frontend/templates/email/dkim.html
+++ b/email_server/server_web_ui/templates/dkim.html
@@ -6,6 +6,7 @@