fixed layout - rename functions for user to sender

This commit is contained in:
nahakubuilde
2025-06-10 01:50:35 +01:00
parent a0dfe8a535
commit f07b9c2150
15 changed files with 352 additions and 238 deletions

View File

@@ -1,2 +1,2 @@
FLASK_APP=app:create_app FLASK_APP=app:flask_app
FLASK_ENV=development FLASK_ENV=development

124
app.py
View File

@@ -31,7 +31,7 @@ sys.path.append(os.path.dirname(os.path.abspath(__file__)))
# Import SMTP server components # Import SMTP server components
from email_server.server_runner import start_server from email_server.server_runner import start_server
from email_server.models import create_tables, Session, Domain, User, WhitelistedIP, DKIMKey, hash_password from email_server.models import create_tables, Session, Domain, Sender, WhitelistedIP, DKIMKey, hash_password
from email_server.settings_loader import load_settings from email_server.settings_loader import load_settings
from email_server.tool_box import get_logger from email_server.tool_box import get_logger
from email_server.dkim_manager import DKIMManager from email_server.dkim_manager import DKIMManager
@@ -91,7 +91,7 @@ class SMTPServerApp:
db = SQLAlchemy(app) db = SQLAlchemy(app)
# Import existing models and register them with Flask-SQLAlchemy # Import existing models and register them with Flask-SQLAlchemy
from email_server.models import Base, Domain, User, WhitelistedIP, DKIMKey, EmailLog, AuthLog, CustomHeader from email_server.models import Base, Domain, Sender, WhitelistedIP, DKIMKey, EmailLog, AuthLog, CustomHeader
# Set the metadata for Flask-Migrate to use existing models # Set the metadata for Flask-Migrate to use existing models
db.Model.metadata = Base.metadata db.Model.metadata = Base.metadata
@@ -109,71 +109,7 @@ class SMTPServerApp:
def index(): def index():
"""Redirect root to email dashboard""" """Redirect root to email dashboard"""
return redirect(url_for('email.dashboard')) return redirect(url_for('email.dashboard'))
@app.route('/health')
def health_check():
"""Health check endpoint"""
return jsonify({
'status': 'healthy',
'timestamp': datetime.now(ZoneInfo('Europe/London')).isoformat(),
'services': {
'smtp_server': 'running' if self.smtp_task and not self.smtp_task.done() else 'stopped',
'web_frontend': 'running'
},
'version': '1.0.0'
})
@app.route('/api/server/status')
def server_status():
"""Get detailed server status"""
session = Session()
try:
status = {
'smtp_server': {
'running': self.smtp_task and not self.smtp_task.done(),
'port': int(self.settings.get('Server', 'SMTP_PORT', fallback=25)),
'tls_port': int(self.settings.get('Server', 'SMTP_TLS_PORT', fallback=587)),
'hostname': self.settings.get('Server', 'hostname', fallback='localhost')
},
'database': {
'domains': session.query(Domain).filter_by(is_active=True).count(),
'users': session.query(User).filter_by(is_active=True).count(),
'dkim_keys': session.query(DKIMKey).filter_by(is_active=True).count(),
'whitelisted_ips': session.query(WhitelistedIP).filter_by(is_active=True).count()
},
'settings': {
'relay_enabled': self.settings.getboolean('Relay', 'enable_relay', fallback=False),
'tls_enabled': self.settings.getboolean('TLS', 'enable_tls', fallback=True),
'dkim_enabled': self.settings.getboolean('DKIM', 'enable_dkim', fallback=True)
}
}
return jsonify(status)
finally:
session.close()
@app.route('/api/server/restart', methods=['POST'])
def restart_server():
"""Restart the SMTP server (API endpoint) via systemd."""
try:
# Only allow from localhost for security
if request.remote_addr not in ('127.0.0.1', '::1'):
return jsonify({'status': 'error', 'message': 'Unauthorized'}), 403
# Restart the systemd service for SMTP (update service name as needed)
result = subprocess.run(
['systemctl', '--user', 'restart', 'pymta-smtp.service'],
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
text=True
)
if result.returncode == 0:
return jsonify({'status': 'success', 'message': 'SMTP server restart requested.'})
else:
return jsonify({'status': 'error', 'message': f'Failed to restart: {result.stderr}'}), 500
except Exception as e:
logger.error(f"Error restarting server: {e}")
return jsonify({'status': 'error', 'message': str(e)}), 500
# Error handlers # Error handlers
@app.errorhandler(404) @app.errorhandler(404)
def not_found_error(error): def not_found_error(error):
@@ -192,6 +128,11 @@ class SMTPServerApp:
error_message="Internal server error", error_message="Internal server error",
error_details=str(error)), 500 error_details=str(error)), 500
@app.route('/health')
def health_check():
"""Health check endpoint"""
return jsonify(self.check_health())
# Context processors for templates # Context processors for templates
@app.context_processor @app.context_processor
def utility_processor(): def utility_processor():
@@ -203,6 +144,7 @@ class SMTPServerApp:
'zip': zip, 'zip': zip,
'str': str, 'str': str,
'int': int, 'int': int,
'check_health': self.check_health
} }
self.flask_app = app self.flask_app = app
@@ -235,24 +177,24 @@ class SMTPServerApp:
for domain_name in sample_domains: for domain_name in sample_domains:
dkim_manager.generate_dkim_keypair(domain_name) dkim_manager.generate_dkim_keypair(domain_name)
# Add sample users # Add sample senders
sample_users = [ sample_senders = [
('admin@example.com', 'example.com', 'admin123', False), ('admin@example.com', 'example.com', 'admin123', False),
] ]
for email, domain_name, password, can_send_as_domain in sample_users: for email, domain_name, password, can_send_as_domain in sample_senders:
existing = session.query(User).filter_by(email=email).first() existing = session.query(Sender).filter_by(email=email).first()
if not existing: if not existing:
domain = session.query(Domain).filter_by(domain_name=domain_name).first() domain = session.query(Domain).filter_by(domain_name=domain_name).first()
if domain: if domain:
user = User( sender = Sender(
email=email, email=email,
password_hash=hash_password(password), password_hash=hash_password(password),
domain_id=domain.id, domain_id=domain.id,
can_send_as_domain=can_send_as_domain can_send_as_domain=can_send_as_domain
) )
session.add(user) session.add(sender)
logger.info(f"Added sample user: {email}") logger.info(f"Added sample sender: {email}")
# Add sample whitelisted IPs # Add sample whitelisted IPs
sample_ips = [ sample_ips = [
@@ -374,6 +316,42 @@ class SMTPServerApp:
finally: finally:
self.shutdown_requested = True self.shutdown_requested = True
def check_health(self):
"""Check the health of all services"""
status = {
'status': 'healthy',
'timestamp': datetime.now(ZoneInfo('Europe/London')).isoformat(),
'services': {
'smtp_server': 'running' if self.smtp_task and not self.smtp_task.done() else 'stopped',
'web_frontend': 'running',
'database': 'ok'
},
'version': '1.0.0'
}
# Check database connection
try:
session = Session()
# Try to query a simple table to verify connection
session.query(Domain).first()
session.close()
except Exception as e:
status['services']['database'] = 'error'
status['status'] = 'degraded'
logger.error(f"Database health check failed: {e}")
# Try to reconnect to database
try:
create_tables()
logger.info("Database reconnection attempted")
except Exception as reconnect_error:
logger.error(f"Database reconnection failed: {reconnect_error}")
# If any service is not running, set overall status to degraded
if status['services']['smtp_server'] == 'stopped' or status['services']['database'] == 'error':
status['status'] = 'degraded'
return status
def main(): def main():
"""Main function""" """Main function"""

View File

@@ -11,8 +11,8 @@ Security Features:
from datetime import datetime from datetime import datetime
from aiosmtpd.smtp import AuthResult, LoginPassword from aiosmtpd.smtp import AuthResult, LoginPassword
from email_server.models import ( from email_server.models import (
Session, User, Domain, WhitelistedIP, Session, Sender, Domain, WhitelistedIP,
check_password, log_auth_attempt, get_user_by_email, check_password, log_auth_attempt, get_sender_by_email,
get_whitelisted_ip, get_domain_by_name get_whitelisted_ip, get_domain_by_name
) )
from email_server.tool_box import get_logger from email_server.tool_box import get_logger
@@ -47,42 +47,38 @@ class EnhancedAuthenticator:
logger.debug(f'Authentication attempt: {username} from {peer_ip}') logger.debug(f'Authentication attempt: {username} from {peer_ip}')
try: try:
# Look up user in database # Look up sender in database
user = get_user_by_email(username) sender = get_sender_by_email(username)
if sender and check_password(password, sender.password_hash):
if user and check_password(password, user.password_hash): # Store authenticated sender info in session for later validation
# Store authenticated user info in session for later validation session.authenticated_sender = sender
session.authenticated_user = user session.auth_type = 'sender'
session.auth_type = 'user'
# Log successful authentication # Log successful authentication
log_auth_attempt( log_auth_attempt(
auth_type='user', auth_type='sender',
identifier=username, identifier=username,
ip_address=peer_ip, ip_address=peer_ip,
success=True, success=True,
message=f'Successful user authentication' message=f'Successful sender authentication'
) )
logger.info(f'Authenticated sender: {username} (ID: {sender.id}, can_send_as_domain: {sender.can_send_as_domain})')
logger.info(f'Authenticated user: {username} (ID: {user.id}, can_send_as_domain: {user.can_send_as_domain})')
return AuthResult(success=True, handled=True) return AuthResult(success=True, handled=True)
else: else:
# Log failed authentication # Log failed authentication
log_auth_attempt( log_auth_attempt(
auth_type='user', auth_type='sender',
identifier=username, identifier=username,
ip_address=peer_ip, ip_address=peer_ip,
success=False, success=False,
message=f'Invalid credentials for {username}' message=f'Invalid credentials for {username}'
) )
logger.warning(f'Authentication failed for {username}: invalid credentials') logger.warning(f'Authentication failed for {username}: invalid credentials')
return AuthResult(success=False, handled=True, message='535 Authentication failed') return AuthResult(success=False, handled=True, message='535 Authentication failed')
except Exception as e: except Exception as e:
logger.error(f'Authentication error for {username}: {e}') logger.error(f'Authentication error for {username}: {e}')
log_auth_attempt( log_auth_attempt(
auth_type='user', auth_type='sender',
identifier=username, identifier=username,
ip_address=peer_ip, ip_address=peer_ip,
success=False, success=False,
@@ -145,19 +141,18 @@ def validate_sender_authorization(session, mail_from: str) -> tuple[bool, str]:
peer_ip = session.peer[0] peer_ip = session.peer[0]
# Check user authentication # Check sender authentication
if hasattr(session, 'authenticated_user') and session.authenticated_user: if hasattr(session, 'authenticated_sender') and session.authenticated_sender:
user = session.authenticated_user sender = session.authenticated_sender
if sender.can_send_as(mail_from):
if user.can_send_as(mail_from): logger.info(f"Sender {sender.email} authorized to send as {mail_from}")
logger.info(f"User {user.email} authorized to send as {mail_from}") return True, f"Sender authorized to send as {mail_from}"
return True, f"User authorized to send as {mail_from}"
else: else:
message = f"User {user.email} not authorized to send as {mail_from}" message = f"Sender {sender.email} not authorized to send as {mail_from}"
logger.warning(message) logger.warning(message)
log_auth_attempt( log_auth_attempt(
auth_type='sender_validation', auth_type='sender_validation',
identifier=f"{user.email} -> {mail_from}", identifier=f"{sender.email} -> {mail_from}",
ip_address=peer_ip, ip_address=peer_ip,
success=False, success=False,
message=message message=message
@@ -205,8 +200,8 @@ def get_authenticated_domain_id(session) -> int:
Returns: Returns:
Domain ID or None if not authenticated Domain ID or None if not authenticated
""" """
if hasattr(session, 'authenticated_user') and session.authenticated_user: if hasattr(session, 'authenticated_sender') and session.authenticated_sender:
return session.authenticated_user.domain_id return session.authenticated_sender.domain_id
if hasattr(session, 'authorized_domain') and session.authorized_domain: if hasattr(session, 'authorized_domain') and session.authorized_domain:
domain = get_domain_by_name(session.authorized_domain) domain = get_domain_by_name(session.authorized_domain)

View File

@@ -38,7 +38,7 @@ class Domain(Base):
created_at = Column(DateTime, default=func.now()) created_at = Column(DateTime, default=func.now())
# Add relationships with proper foreign key references # Add relationships with proper foreign key references
users = relationship("User", backref="domain", lazy="joined") senders = relationship("Sender", backref="domain", lazy="joined")
dkim_keys = relationship("DKIMKey", backref="domain", lazy="joined") dkim_keys = relationship("DKIMKey", backref="domain", lazy="joined")
whitelisted_ips = relationship("WhitelistedIP", backref="domain", lazy="joined") whitelisted_ips = relationship("WhitelistedIP", backref="domain", lazy="joined")
custom_headers = relationship("CustomHeader", backref="domain", lazy="joined") custom_headers = relationship("CustomHeader", backref="domain", lazy="joined")
@@ -46,15 +46,15 @@ class Domain(Base):
def __repr__(self): def __repr__(self):
return f"<Domain(id={self.id}, domain_name='{self.domain_name}', active={self.is_active})>" return f"<Domain(id={self.id}, domain_name='{self.domain_name}', active={self.is_active})>"
class User(Base): class Sender(Base):
""" """
User model with enhanced authentication controls. Sender model with enhanced authentication controls.
Security features: Security features:
- can_send_as_domain: If True, user can send as any email from their domain - can_send_as_domain: If True, sender can send as any email from their domain
- If False, user can only send as their own email address - If False, sender can only send as their own email address
""" """
__tablename__ = 'esrv_users' __tablename__ = 'esrv_senders'
id = Column(Integer, primary_key=True) id = Column(Integer, primary_key=True)
email = Column(String, unique=True, nullable=False) email = Column(String, unique=True, nullable=False)
@@ -66,28 +66,25 @@ class User(Base):
def can_send_as(self, from_address: str) -> bool: def can_send_as(self, from_address: str) -> bool:
""" """
Check if this user can send emails as the given from_address. Check if this sender can send emails as the given from_address.
Args: Args:
from_address: The email address the user wants to send from from_address: The email address the sender wants to send from
Returns: Returns:
True if user is allowed to send as this address True if sender is allowed to send as this address
""" """
# User can always send as their own email # Sender can always send as their own email
if from_address.lower() == self.email.lower(): if from_address.lower() == self.email.lower():
return True return True
# If sender has domain privileges, check if from_address is from same domain
# If user has domain privileges, check if from_address is from same domain
if self.can_send_as_domain: if self.can_send_as_domain:
user_domain = self.email.split('@')[1].lower() sender_domain = self.email.split('@')[1].lower()
from_domain = from_address.split('@')[1].lower() if '@' in from_address else '' from_domain = from_address.split('@')[1].lower() if '@' in from_address else ''
return user_domain == from_domain return sender_domain == from_domain
return False return False
def __repr__(self): def __repr__(self):
return f"<User(id={self.id}, email='{self.email}', domain_id={self.domain_id}, can_send_as_domain={self.can_send_as_domain})>" return f"<Sender(id={self.id}, email='{self.email}', domain_id={self.domain_id}, can_send_as_domain={self.can_send_as_domain})>"
class WhitelistedIP(Base): class WhitelistedIP(Base):
""" """
@@ -276,11 +273,11 @@ def log_email(from_address: str, to_address: str, subject: str,
finally: finally:
session.close() session.close()
def get_user_by_email(email: str): def get_sender_by_email(email: str):
"""Get user by email address.""" """Get sender by email address."""
session = Session() session = Session()
try: try:
return session.query(User).filter_by(email=email.lower(), is_active=True).first() return session.query(Sender).filter_by(email=email.lower(), is_active=True).first()
finally: finally:
session.close() session.close()

View File

@@ -46,7 +46,7 @@ async def start_server(shutdown_event=None):
# dkim_manager.initialize_default_keys() # Removed: do not auto-generate DKIM keys for all domains # dkim_manager.initialize_default_keys() # Removed: do not auto-generate DKIM keys for all domains
# Add test data if needed # Add test data if needed
from .models import Session, Domain, User, WhitelistedIP, hash_password from .models import Session, Domain, Sender, WhitelistedIP, hash_password
session = Session() session = Session()
try: try:
# Add example.com domain if not exists # Add example.com domain if not exists
@@ -57,17 +57,17 @@ async def start_server(shutdown_event=None):
session.commit() session.commit()
logger.debug("Added example.com domain") logger.debug("Added example.com domain")
# Add test user if not exists # Add test sender if not exists
user = session.query(User).filter_by(email='test@example.com').first() sender = session.query(Sender).filter_by(email='test@example.com').first()
if not user: if not sender:
user = User( sender = Sender(
email='test@example.com', email='test@example.com',
password_hash=hash_password('testpass123'), password_hash=hash_password('testpass123'),
domain_id=domain.id domain_id=domain.id
) )
session.add(user) session.add(sender)
session.commit() session.commit()
logger.debug("Added test user: test@example.com") logger.debug("Added test sender: test@example.com")
# Add whitelisted IP if not exists # Add whitelisted IP if not exists
whitelist = session.query(WhitelistedIP).filter_by(ip_address='127.0.0.1').first() whitelist = session.query(WhitelistedIP).filter_by(ip_address='127.0.0.1').first()

View File

@@ -5,7 +5,7 @@ This module provides the main dashboard view and overview functionality.
""" """
from flask import render_template from flask import render_template
from email_server.models import Session, Domain, User, DKIMKey, EmailLog, AuthLog from email_server.models import Session, Domain, Sender, DKIMKey, EmailLog, AuthLog
from email_server.tool_box import get_logger from email_server.tool_box import get_logger
from .routes import email_bp from .routes import email_bp
@@ -19,7 +19,7 @@ def dashboard():
try: try:
# Get counts # Get counts
domain_count = session.query(Domain).filter_by(is_active=True).count() domain_count = session.query(Domain).filter_by(is_active=True).count()
user_count = session.query(User).filter_by(is_active=True).count() sender_count = session.query(Sender).filter_by(is_active=True).count()
dkim_count = session.query(DKIMKey).filter_by(is_active=True).count() dkim_count = session.query(DKIMKey).filter_by(is_active=True).count()
# Get recent email logs # Get recent email logs
@@ -30,7 +30,7 @@ def dashboard():
return render_template('dashboard.html', return render_template('dashboard.html',
domain_count=domain_count, domain_count=domain_count,
user_count=user_count, sender_count=sender_count,
dkim_count=dkim_count, dkim_count=dkim_count,
recent_emails=recent_emails, recent_emails=recent_emails,
recent_auths=recent_auths) recent_auths=recent_auths)

View File

@@ -432,11 +432,22 @@ def check_spf_dns():
spf_record = record spf_record = record
break break
spf_valid_for_server = False
spf_check_message = ''
public_ip = get_public_ip()
ip_mechanism = f'ip4:{public_ip}'
if spf_record: if spf_record:
result['spf_record'] = spf_record result['spf_record'] = spf_record
if ip_mechanism in spf_record:
spf_valid_for_server = True
spf_check_message = f'SPF is valid for this server (contains {ip_mechanism})'
else:
spf_check_message = f'SPF is missing this server\'s IP ({ip_mechanism})'
result['message'] = 'SPF record found' result['message'] = 'SPF record found'
else: else:
result['success'] = False result['success'] = False
result['message'] = 'No SPF record found' result['message'] = 'No SPF record found'
result['spf_valid_for_server'] = spf_valid_for_server
result['spf_check_message'] = spf_check_message
result['public_ip'] = public_ip
return jsonify(result) return jsonify(result)

View File

@@ -10,7 +10,7 @@ This module provides domain management functionality including:
""" """
from flask import render_template, request, redirect, url_for, flash from flask import render_template, request, redirect, url_for, flash
from email_server.models import Session, Domain, User, WhitelistedIP, DKIMKey, CustomHeader from email_server.models import Session, Domain, Sender, WhitelistedIP, DKIMKey, CustomHeader
from email_server.dkim_manager import DKIMManager from email_server.dkim_manager import DKIMManager
from email_server.tool_box import get_logger from email_server.tool_box import get_logger
from sqlalchemy.orm import joinedload from sqlalchemy.orm import joinedload
@@ -188,12 +188,12 @@ def remove_domain(domain_id: int):
domain_name = domain.domain_name domain_name = domain.domain_name
# Count associated records # Count associated records
user_count = session.query(User).filter_by(domain_id=domain_id).count() sender_count = session.query(Sender).filter_by(domain_id=domain_id).count()
ip_count = session.query(WhitelistedIP).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() dkim_count = session.query(DKIMKey).filter_by(domain_id=domain_id).count()
# Delete associated records # Delete associated records
session.query(User).filter_by(domain_id=domain_id).delete() session.query(Sender).filter_by(domain_id=domain_id).delete()
session.query(WhitelistedIP).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(DKIMKey).filter_by(domain_id=domain_id).delete()
session.query(CustomHeader).filter_by(domain_id=domain_id).delete() session.query(CustomHeader).filter_by(domain_id=domain_id).delete()
@@ -202,7 +202,7 @@ def remove_domain(domain_id: int):
session.delete(domain) session.delete(domain)
session.commit() session.commit()
flash(f'Domain {domain_name} and all associated data permanently removed ({user_count} users, {ip_count} IPs, {dkim_count} DKIM keys)', 'success') flash(f'Domain {domain_name} and all associated data permanently removed ({sender_count} senders, {ip_count} IPs, {dkim_count} DKIM keys)', 'success')
return redirect(url_for('email.domains_list')) return redirect(url_for('email.domains_list'))
except Exception as e: except Exception as e:

View File

@@ -10,27 +10,27 @@ This module provides sender management functionality including:
""" """
from flask import render_template, request, redirect, url_for, flash from flask import render_template, request, redirect, url_for, flash
from email_server.models import Session, Domain, User from email_server.models import Session, Domain, Sender
from email_server.tool_box import get_logger from email_server.tool_box import get_logger
import bcrypt import bcrypt
from .routes import email_bp from .routes import email_bp
from email_server.models import Session, Domain, User, hash_password from email_server.models import Session, Domain, Sender, hash_password
logger = get_logger() logger = get_logger()
@email_bp.route('/senders') @email_bp.route('/senders')
def senders_list(): def senders_list():
"""List all users.""" """List all senders."""
session = Session() session = Session()
try: try:
users = session.query(User, Domain).join(Domain, User.domain_id == Domain.id).order_by(User.email).all() senders = session.query(Sender, Domain).join(Domain, Sender.domain_id == Domain.id).order_by(Sender.email).all()
return render_template('senders.html', users=users) return render_template('senders.html', senders=senders)
finally: finally:
session.close() session.close()
@email_bp.route('/senders/add', methods=['GET', 'POST']) @email_bp.route('/senders/add', methods=['GET', 'POST'])
def add_sender(): def add_sender():
"""Add new user.""" """Add new sender."""
session = Session() session = Session()
try: try:
domains = session.query(Domain).filter_by(is_active=True).order_by(Domain.domain_name).all() domains = session.query(Domain).filter_by(is_active=True).order_by(Domain.domain_name).all()
@@ -50,118 +50,118 @@ def add_sender():
flash('Invalid email format', 'error') flash('Invalid email format', 'error')
return redirect(url_for('email.add_sender')) return redirect(url_for('email.add_sender'))
# Check if user already exists # Check if sender already exists
existing = session.query(User).filter_by(email=email).first() existing = session.query(Sender).filter_by(email=email).first()
if existing: if existing:
flash(f'User {email} already exists', 'error') flash(f'Sender {email} already exists', 'error')
return redirect(url_for('email.senders_list')) return redirect(url_for('email.senders_list'))
# Create user # Create sender
user = User( sender = Sender(
email=email, email=email,
password_hash=hash_password(password), password_hash=hash_password(password),
domain_id=domain_id, domain_id=domain_id,
can_send_as_domain=can_send_as_domain can_send_as_domain=can_send_as_domain
) )
session.add(user) session.add(sender)
session.commit() session.commit()
flash(f'User {email} added successfully', 'success') flash(f'Sender {email} added successfully', 'success')
return redirect(url_for('email.senders_list')) return redirect(url_for('email.senders_list'))
return render_template('add_sender.html', domains=domains) return render_template('add_sender.html', domains=domains)
except Exception as e: except Exception as e:
session.rollback() session.rollback()
logger.error(f"Error adding user: {e}") logger.error(f"Error adding sender: {e}")
flash(f'Error adding user: {str(e)}', 'error') flash(f'Error adding sender: {str(e)}', 'error')
return redirect(url_for('email.add_sender')) return redirect(url_for('email.add_sender'))
finally: finally:
session.close() session.close()
@email_bp.route('/senders/<int:user_id>/delete', methods=['POST']) @email_bp.route('/senders/<int:user_id>/delete', methods=['POST'])
def delete_sender(user_id: int): def delete_sender(user_id: int):
"""Disable user (soft delete).""" """Disable sender (soft delete)."""
session = Session() session = Session()
try: try:
user = session.query(User).get(user_id) sender = session.query(Sender).get(user_id)
if not user: if not sender:
flash('User not found', 'error') flash('Sender not found', 'error')
return redirect(url_for('email.senders_list')) return redirect(url_for('email.senders_list'))
user_email = user.email sender_email = sender.email
user.is_active = False sender.is_active = False
session.commit() session.commit()
flash(f'User {user_email} disabled', 'success') flash(f'Sender {sender_email} disabled', 'success')
return redirect(url_for('email.senders_list')) return redirect(url_for('email.senders_list'))
except Exception as e: except Exception as e:
session.rollback() session.rollback()
logger.error(f"Error disabling user: {e}") logger.error(f"Error disabling sender: {e}")
flash(f'Error disabling user: {str(e)}', 'error') flash(f'Error disabling sender: {str(e)}', 'error')
return redirect(url_for('email.senders_list')) return redirect(url_for('email.senders_list'))
finally: finally:
session.close() session.close()
@email_bp.route('/senders/<int:user_id>/enable', methods=['POST']) @email_bp.route('/senders/<int:user_id>/enable', methods=['POST'])
def enable_sender(user_id: int): def enable_sender(user_id: int):
"""Enable user.""" """Enable sender."""
session = Session() session = Session()
try: try:
user = session.query(User).get(user_id) sender = session.query(Sender).get(user_id)
if not user: if not sender:
flash('User not found', 'error') flash('Sender not found', 'error')
return redirect(url_for('email.senders_list')) return redirect(url_for('email.senders_list'))
user_email = user.email sender_email = sender.email
user.is_active = True sender.is_active = True
session.commit() session.commit()
flash(f'User {user_email} enabled', 'success') flash(f'Sender {sender_email} enabled', 'success')
return redirect(url_for('email.senders_list')) return redirect(url_for('email.senders_list'))
except Exception as e: except Exception as e:
session.rollback() session.rollback()
logger.error(f"Error enabling user: {e}") logger.error(f"Error enabling sender: {e}")
flash(f'Error enabling user: {str(e)}', 'error') flash(f'Error enabling sender: {str(e)}', 'error')
return redirect(url_for('email.senders_list')) return redirect(url_for('email.senders_list'))
finally: finally:
session.close() session.close()
@email_bp.route('/senders/<int:user_id>/remove', methods=['POST']) @email_bp.route('/senders/<int:user_id>/remove', methods=['POST'])
def remove_sender(user_id: int): def remove_sender(user_id: int):
"""Permanently remove user.""" """Permanently remove sender."""
session = Session() session = Session()
try: try:
user = session.query(User).get(user_id) sender = session.query(Sender).get(user_id)
if not user: if not sender:
flash('User not found', 'error') flash('Sender not found', 'error')
return redirect(url_for('email.senders_list')) return redirect(url_for('email.senders_list'))
user_email = user.email sender_email = sender.email
session.delete(user) session.delete(sender)
session.commit() session.commit()
flash(f'User {user_email} permanently removed', 'success') flash(f'Sender {sender_email} permanently removed', 'success')
return redirect(url_for('email.senders_list')) return redirect(url_for('email.senders_list'))
except Exception as e: except Exception as e:
session.rollback() session.rollback()
logger.error(f"Error removing user: {e}") logger.error(f"Error removing sender: {e}")
flash(f'Error removing user: {str(e)}', 'error') flash(f'Error removing sender: {str(e)}', 'error')
return redirect(url_for('email.senders_list')) return redirect(url_for('email.senders_list'))
finally: finally:
session.close() session.close()
@email_bp.route('/senders/<int:user_id>/edit', methods=['GET', 'POST']) @email_bp.route('/senders/<int:user_id>/edit', methods=['GET', 'POST'])
def edit_sender(user_id: int): def edit_sender(user_id: int):
"""Edit user.""" """Edit sender."""
session = Session() session = Session()
try: try:
user = session.query(User).get(user_id) sender = session.query(Sender).get(user_id)
if not user: if not sender:
flash('User not found', 'error') flash('Sender not found', 'error')
return redirect(url_for('email.senders_list')) return redirect(url_for('email.senders_list'))
domains = session.query(Domain).filter_by(is_active=True).order_by(Domain.domain_name).all() domains = session.query(Domain).filter_by(is_active=True).order_by(Domain.domain_name).all()
@@ -181,35 +181,35 @@ def edit_sender(user_id: int):
flash('Invalid email format', 'error') flash('Invalid email format', 'error')
return redirect(url_for('email.edit_sender', user_id=user_id)) return redirect(url_for('email.edit_sender', user_id=user_id))
# Check if email already exists (excluding current user) # Check if email already exists (excluding current sender)
existing = session.query(User).filter( existing = session.query(Sender).filter(
User.email == email, Sender.email == email,
User.id != user_id Sender.id != user_id
).first() ).first()
if existing: if existing:
flash(f'Email {email} already exists', 'error') flash(f'Email {email} already exists', 'error')
return redirect(url_for('email.edit_sender', user_id=user_id)) return redirect(url_for('email.edit_sender', user_id=user_id))
# Update user # Update sender
user.email = email sender.email = email
user.domain_id = domain_id sender.domain_id = domain_id
user.can_send_as_domain = can_send_as_domain sender.can_send_as_domain = can_send_as_domain
# Update password if provided # Update password if provided
if password: if password:
user.password_hash = hash_password(password) sender.password_hash = hash_password(password)
session.commit() session.commit()
flash(f'User {email} updated successfully', 'success') flash(f'Sender {email} updated successfully', 'success')
return redirect(url_for('email.senders_list')) return redirect(url_for('email.senders_list'))
return render_template('edit_sender.html', user=user, domains=domains) return render_template('edit_sender.html', sender=sender, domains=domains)
except Exception as e: except Exception as e:
session.rollback() session.rollback()
logger.error(f"Error editing user: {e}") logger.error(f"Error editing sender: {e}")
flash(f'Error editing user: {str(e)}', 'error') flash(f'Error editing sender: {str(e)}', 'error')
return redirect(url_for('email.senders_list')) return redirect(url_for('email.senders_list'))
finally: finally:
session.close() session.close()

View File

@@ -131,6 +131,20 @@
<!-- Custom SMTP Management CSS --> <!-- Custom SMTP Management CSS -->
<link href="{{ url_for('email.static', filename='css/smtp-management.css') }}" rel="stylesheet"> <link href="{{ url_for('email.static', filename='css/smtp-management.css') }}" rel="stylesheet">
<style>
/* Ensure tooltip text is visible on dark backgrounds */
.tooltip-inner {
color: #fff !important;
background-color: #222 !important;
font-size: 1rem;
text-align: left;
}
.bs-tooltip-auto[data-popper-placement^=top] .tooltip-arrow::before,
.bs-tooltip-top .tooltip-arrow::before {
border-top-color: #222 !important;
}
</style>
{% block extra_css %}{% endblock %} {% block extra_css %}{% endblock %}
</head> </head>
<body> <body>
@@ -337,6 +351,16 @@
<!-- Custom SMTP Management JavaScript --> <!-- Custom SMTP Management JavaScript -->
<script src="{{ url_for('email.static', filename='js/smtp-management.js') }}"></script> <script src="{{ url_for('email.static', filename='js/smtp-management.js') }}"></script>
<!-- Bootstrap Tooltip Initialization -->
<script>
document.addEventListener('DOMContentLoaded', function() {
var tooltipTriggerList = [].slice.call(document.querySelectorAll('[data-bs-toggle="tooltip"]'));
tooltipTriggerList.forEach(function (tooltipTriggerEl) {
new bootstrap.Tooltip(tooltipTriggerEl);
});
});
</script>
{% block extra_js %}{% endblock %} {% block extra_js %}{% endblock %}
</body> </body>
</html> </html>

View File

@@ -7,6 +7,7 @@
<div class="row"> <div class="row">
<!-- Statistics Cards --> <!-- Statistics Cards -->
<div class="col-lg-3 col-md-6 mb-4"> <div class="col-lg-3 col-md-6 mb-4">
<a href="{{ url_for('email.domains_list') }}" class="dashboard-card-link text-decoration-none">
<div class="card border-primary"> <div class="card border-primary">
<div class="card-body"> <div class="card-body">
<div class="d-flex align-items-center"> <div class="d-flex align-items-center">
@@ -24,19 +25,21 @@
</div> </div>
</div> </div>
</div> </div>
</a>
</div> </div>
<div class="col-lg-3 col-md-6 mb-4"> <div class="col-lg-3 col-md-6 mb-4">
<a href="{{ url_for('email.senders_list') }}" class="dashboard-card-link text-decoration-none">
<div class="card border-success"> <div class="card border-success">
<div class="card-body"> <div class="card-body">
<div class="d-flex align-items-center"> <div class="d-flex align-items-center">
<div class="flex-grow-1"> <div class="flex-grow-1">
<h5 class="card-title text-success mb-1"> <h5 class="card-title text-success mb-1">
<i class="bi bi-people me-2"></i> <i class="bi bi-people me-2"></i>
Users Senders
</h5> </h5>
<h3 class="mb-0">{{ user_count }}</h3> <h3 class="mb-0">{{ sender_count }}</h3>
<small class="text-muted">Authenticated users</small> <small class="text-muted">Authenticated senders</small>
</div> </div>
<div class="fs-2 text-success opacity-50"> <div class="fs-2 text-success opacity-50">
<i class="bi bi-people"></i> <i class="bi bi-people"></i>
@@ -44,9 +47,11 @@
</div> </div>
</div> </div>
</div> </div>
</a>
</div> </div>
<div class="col-lg-3 col-md-6 mb-4"> <div class="col-lg-3 col-md-6 mb-4">
<a href="{{ url_for('email.dkim_list') }}" class="dashboard-card-link text-decoration-none">
<div class="card border-warning"> <div class="card border-warning">
<div class="card-body"> <div class="card-body">
<div class="d-flex align-items-center"> <div class="d-flex align-items-center">
@@ -64,6 +69,7 @@
</div> </div>
</div> </div>
</div> </div>
</a>
</div> </div>
<div class="col-lg-3 col-md-6 mb-4"> <div class="col-lg-3 col-md-6 mb-4">
@@ -75,11 +81,28 @@
<i class="bi bi-activity me-2"></i> <i class="bi bi-activity me-2"></i>
Status Status
</h5> </h5>
<h6 class="text-success mb-0"> {% set health = check_health() %}
<h6 class="{% if health.status == 'healthy' %}text-success{% else %}text-warning{% endif %} mb-0 status-indicator"
data-bs-toggle="tooltip"
data-bs-html="true"
data-bs-placement="top"
title="{{ (
"<div class='text-start'><strong>Service Status:</strong><br>" +
'SMTP Server: ' + health.services.smtp_server|title + '<br>' +
'Web Frontend: ' + health.services.web_frontend|title + '<br>' +
'Database: ' + health.services.database|title + '</div>'
) | safe }}">
<i class="bi bi-circle-fill me-1" style="font-size: 0.5rem;"></i> <i class="bi bi-circle-fill me-1" style="font-size: 0.5rem;"></i>
Online {{ health.status|title }}
</h6> </h6>
<small class="text-muted">Server running</small> <small class="text-muted">
{% if health.services.smtp_server == 'running' and health.services.database == 'ok' %}
All services running
{% else %}
{% if health.services.smtp_server == 'stopped' %}SMTP Server stopped{% endif %}
{% if health.services.database == 'error' %}Database error{% endif %}
{% endif %}
</small>
</div> </div>
<div class="fs-2 text-info opacity-50"> <div class="fs-2 text-info opacity-50">
<i class="bi bi-activity"></i> <i class="bi bi-activity"></i>
@@ -248,7 +271,7 @@
<div class="d-grid"> <div class="d-grid">
<a href="{{ url_for('email.add_sender') }}" class="btn btn-outline-success"> <a href="{{ url_for('email.add_sender') }}" class="btn btn-outline-success">
<i class="bi bi-person-plus me-2"></i> <i class="bi bi-person-plus me-2"></i>
Add User Add Sender
</a> </a>
</div> </div>
</div> </div>
@@ -282,4 +305,31 @@
location.reload(); location.reload();
}, 30000); }, 30000);
</script> </script>
<style>
.status-indicator {
cursor: pointer;
}
.dashboard-card-link {
cursor: pointer;
display: block;
}
.dashboard-card-link:hover .card {
box-shadow: 0 0 0 2px #0d6efd33;
filter: brightness(1.05);
}
</style>
<script>
document.addEventListener('DOMContentLoaded', function() {
const tooltipTriggerList = document.querySelectorAll('[data-bs-toggle="tooltip"]');
tooltipTriggerList.forEach(function(tooltipTriggerEl) {
new bootstrap.Tooltip(tooltipTriggerEl, {
html: true,
placement: 'top',
trigger: 'hover'
});
});
});
</script>
{% endblock %} {% endblock %}

View File

@@ -331,6 +331,30 @@
{% block extra_js %} {% block extra_js %}
<script> <script>
// Clipboard copy function for all copy buttons
function copyToClipboard(text) {
if (navigator.clipboard) {
navigator.clipboard.writeText(text).then(function() {
showToast('Copied to clipboard!', 'success');
}, function(err) {
showToast('Failed to copy: ' + err, 'danger');
});
} else {
// Fallback for older browsers
const textarea = document.createElement('textarea');
textarea.value = text;
document.body.appendChild(textarea);
textarea.select();
try {
document.execCommand('copy');
showToast('Copied to clipboard!', 'success');
} catch (err) {
showToast('Failed to copy: ' + err, 'danger');
}
document.body.removeChild(textarea);
}
}
// Show toast notification // Show toast notification
function showToast(message, type = 'info') { function showToast(message, type = 'info') {
const toastContainer = document.getElementById('toastContainer'); const toastContainer = document.getElementById('toastContainer');
@@ -433,6 +457,14 @@
// Update SPF status // Update SPF status
if (spfResult.success) { if (spfResult.success) {
spfStatus.innerHTML = '<span class="status-indicator status-success"></span><small class="text-success">✓ Found</small>'; spfStatus.innerHTML = '<span class="status-indicator status-success"></span><small class="text-success">✓ Found</small>';
// Show additional SPF check message if available
if (typeof spfResult.spf_valid_for_server !== 'undefined') {
if (spfResult.spf_valid_for_server) {
spfStatus.innerHTML += '<br><span class="text-success"><i class="bi bi-check-circle me-1"></i> SPF is valid for this server</span>';
} else {
spfStatus.innerHTML += '<br><span class="text-warning"><i class="bi bi-exclamation-triangle me-1"></i> SPF missing server IP</span>';
}
}
} else { } else {
spfStatus.innerHTML = '<span class="status-indicator status-danger"></span><small class="text-danger">✗ Not found</small>'; spfStatus.innerHTML = '<span class="status-indicator status-danger"></span><small class="text-danger">✗ Not found</small>';
} }

View File

@@ -1,6 +1,6 @@
{% extends "base.html" %} {% extends "base.html" %}
{% block title %}Edit User - SMTP Management{% endblock %} {% block title %}Edit Sender - SMTP Management{% endblock %}
{% block content %} {% block content %}
<div class="row"> <div class="row">
@@ -9,7 +9,7 @@
<div class="card-header"> <div class="card-header">
<h5 class="mb-0"> <h5 class="mb-0">
<i class="bi bi-person-fill-gear me-2"></i> <i class="bi bi-person-fill-gear me-2"></i>
Edit User Edit Sender
</h5> </h5>
</div> </div>
<div class="card-body"> <div class="card-body">
@@ -20,7 +20,7 @@
class="form-control" class="form-control"
id="email" id="email"
name="email" name="email"
value="{{ user.email }}" value="{{ sender.email }}"
required> required>
</div> </div>
@@ -42,7 +42,7 @@
<option value="">Select a domain</option> <option value="">Select a domain</option>
{% for domain in domains %} {% for domain in domains %}
<option value="{{ domain.id }}" <option value="{{ domain.id }}"
{% if domain.id == user.domain_id %}selected{% endif %}> {% if domain.id == sender.domain_id %}selected{% endif %}>
{{ domain.domain_name }} {{ domain.domain_name }}
</option> </option>
{% endfor %} {% endfor %}
@@ -55,12 +55,12 @@
type="checkbox" type="checkbox"
id="can_send_as_domain" id="can_send_as_domain"
name="can_send_as_domain" name="can_send_as_domain"
{% if user.can_send_as_domain %}checked{% endif %}> {% if sender.can_send_as_domain %}checked{% endif %}>
<label class="form-check-label" for="can_send_as_domain"> <label class="form-check-label" for="can_send_as_domain">
<strong>Can send as any email from domain</strong> <strong>Can send as any email from domain</strong>
</label> </label>
<div class="form-text"> <div class="form-text">
Allow this user to send emails using any address within their domain Allow this sender to send emails using any address within their domain
</div> </div>
</div> </div>
</div> </div>
@@ -68,7 +68,7 @@
<div class="d-flex gap-2"> <div class="d-flex gap-2">
<button type="submit" class="btn btn-primary"> <button type="submit" class="btn btn-primary">
<i class="bi bi-check-lg me-1"></i> <i class="bi bi-check-lg me-1"></i>
Update User Update Sender
</button> </button>
<a href="{{ url_for('email.senders_list') }}" class="btn btn-secondary"> <a href="{{ url_for('email.senders_list') }}" class="btn btn-secondary">
<i class="bi bi-x-lg me-1"></i> <i class="bi bi-x-lg me-1"></i>
@@ -85,26 +85,26 @@
<div class="card-header"> <div class="card-header">
<h6 class="mb-0"> <h6 class="mb-0">
<i class="bi bi-info-circle me-2"></i> <i class="bi bi-info-circle me-2"></i>
Current User Details Current Sender Details
</h6> </h6>
</div> </div>
<div class="card-body"> <div class="card-body">
<dl class="row mb-0"> <dl class="row mb-0">
<dt class="col-sm-4">Email:</dt> <dt class="col-sm-4">Email:</dt>
<dd class="col-sm-8"> <dd class="col-sm-8">
<code>{{ user.email }}</code> <code>{{ sender.email }}</code>
</dd> </dd>
<dt class="col-sm-4">Domain:</dt> <dt class="col-sm-4">Domain:</dt>
<dd class="col-sm-8"> <dd class="col-sm-8">
{% for domain in domains %} {% for domain in domains %}
{% if domain.id == user.domain_id %} {% if domain.id == sender.domain_id %}
<span class="badge bg-secondary">{{ domain.domain_name }}</span> <span class="badge bg-secondary">{{ domain.domain_name }}</span>
{% endif %} {% endif %}
{% endfor %} {% endfor %}
</dd> </dd>
<dt class="col-sm-4">Status:</dt> <dt class="col-sm-4">Status:</dt>
<dd class="col-sm-8"> <dd class="col-sm-8">
{% if user.is_active %} {% if sender.is_active %}
<span class="badge bg-success"> <span class="badge bg-success">
<i class="bi bi-check-circle me-1"></i> <i class="bi bi-check-circle me-1"></i>
Active Active
@@ -118,7 +118,7 @@
</dd> </dd>
<dt class="col-sm-4">Domain Sender:</dt> <dt class="col-sm-4">Domain Sender:</dt>
<dd class="col-sm-8"> <dd class="col-sm-8">
{% if user.can_send_as_domain %} {% if sender.can_send_as_domain %}
<span class="badge bg-success"> <span class="badge bg-success">
<i class="bi bi-check-circle me-1"></i> <i class="bi bi-check-circle me-1"></i>
Yes Yes
@@ -133,7 +133,7 @@
<dt class="col-sm-4">Created:</dt> <dt class="col-sm-4">Created:</dt>
<dd class="col-sm-8"> <dd class="col-sm-8">
<small class="text-muted"> <small class="text-muted">
{{ user.created_at.strftime('%Y-%m-%d %H:%M') }} {{ sender.created_at.strftime('%Y-%m-%d %H:%M') }}
</small> </small>
</dd> </dd>
</dl> </dl>
@@ -144,7 +144,7 @@
<div class="card-header"> <div class="card-header">
<h6 class="mb-0"> <h6 class="mb-0">
<i class="bi bi-shield-check me-2"></i> <i class="bi bi-shield-check me-2"></i>
User Permissions Sender Permissions
</h6> </h6>
</div> </div>
<div class="card-body"> <div class="card-body">
@@ -154,8 +154,8 @@
Domain Sender Permission Domain Sender Permission
</h6> </h6>
<ul class="mb-0 small"> <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>Enabled:</strong> Sender 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> <li><strong>Disabled:</strong> Sender can only send emails from their own email address</li>
</ul> </ul>
</div> </div>
</div> </div>

View File

@@ -76,7 +76,7 @@
</h5> </h5>
</div> </div>
<div class="card-body p-0"> <div class="card-body p-0">
{% if users %} {% if senders %}
<div class="table-responsive"> <div class="table-responsive">
<table class="table table-dark table-hover mb-0"> <table class="table table-dark table-hover mb-0">
<thead> <thead>
@@ -90,16 +90,16 @@
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
{% for user, domain in users %} {% for sender, domain in senders %}
<tr> <tr>
<td> <td>
<div class="fw-bold">{{ user.email }}</div> <div class="fw-bold">{{ sender.email }}</div>
</td> </td>
<td> <td>
<span class="badge bg-secondary">{{ domain.domain_name }}</span> <span class="badge bg-secondary">{{ domain.domain_name }}</span>
</td> </td>
<td> <td>
{% if user.can_send_as_domain %} {% if sender.can_send_as_domain %}
<span class="badge bg-warning" style="color: black;"> <span class="badge bg-warning" style="color: black;">
<i class="bi bi-star me-1"></i> <i class="bi bi-star me-1"></i>
Domain Sender Domain Sender
@@ -112,11 +112,11 @@
Regular Sender Regular Sender
</span> </span>
<br> <br>
<small class="text-muted">Can only send as {{ user.email }}</small> <small class="text-muted">Can only send as {{ sender.email }}</small>
{% endif %} {% endif %}
</td> </td>
<td> <td>
{% if user.is_active %} {% if sender.is_active %}
<span class="badge bg-success"> <span class="badge bg-success">
<i class="bi bi-check-circle me-1"></i> <i class="bi bi-check-circle me-1"></i>
Active Active
@@ -130,45 +130,45 @@
</td> </td>
<td> <td>
<small class="text-muted"> <small class="text-muted">
{{ user.created_at.strftime('%Y-%m-%d %H:%M') }} {{ sender.created_at.strftime('%Y-%m-%d %H:%M') }}
</small> </small>
</td> </td>
<td> <td>
<div class="btn-group" role="group"> <div class="btn-group" role="group">
<!-- Edit Button --> <!-- Edit Button -->
<a href="{{ url_for('email.edit_sender', user_id=user.id) }}" <a href="{{ url_for('email.edit_sender', user_id=sender.id) }}"
class="btn btn-outline-primary btn-sm" class="btn btn-outline-primary btn-sm"
title="Edit Sender"> title="Edit Sender">
<i class="bi bi-pencil"></i> <i class="bi bi-pencil"></i>
</a> </a>
<!-- Enable/Disable Button --> <!-- Enable/Disable Button -->
{% if user.is_active %} {% if sender.is_active %}
<form method="post" action="{{ url_for('email.delete_sender', user_id=user.id) }}" class="d-inline"> <form method="post" action="{{ url_for('email.delete_sender', user_id=sender.id) }}" class="d-inline">
<button type="submit" <button type="submit"
class="btn btn-outline-warning btn-sm" class="btn btn-outline-warning btn-sm"
title="Disable Sender" title="Disable Sender"
onclick="return confirm('Disable user {{ user.email }}?')"> onclick="return confirm('Disable user {{ sender.email }}?')">
<i class="bi bi-pause-circle"></i> <i class="bi bi-pause-circle"></i>
</button> </button>
</form> </form>
{% else %} {% else %}
<form method="post" action="{{ url_for('email.enable_sender', user_id=user.id) }}" class="d-inline"> <form method="post" action="{{ url_for('email.enable_sender', user_id=sender.id) }}" class="d-inline">
<button type="submit" <button type="submit"
class="btn btn-outline-success btn-sm" class="btn btn-outline-success btn-sm"
title="Enable Sender" title="Enable Sender"
onclick="return confirm('Enable user {{ user.email }}?')"> onclick="return confirm('Enable user {{ sender.email }}?')">
<i class="bi bi-play-circle"></i> <i class="bi bi-play-circle"></i>
</button> </button>
</form> </form>
{% endif %} {% endif %}
<!-- Permanent Remove Button --> <!-- Permanent Remove Button -->
<form method="post" action="{{ url_for('email.remove_sender', user_id=user.id) }}" class="d-inline"> <form method="post" action="{{ url_for('email.remove_sender', user_id=sender.id) }}" class="d-inline">
<button type="submit" <button type="submit"
class="btn btn-outline-danger btn-sm" class="btn btn-outline-danger btn-sm"
title="Permanently Remove Sender" title="Permanently Remove Sender"
onclick="return confirm('Permanently remove user {{ user.email }}? This cannot be undone!')"> onclick="return confirm('Permanently remove user {{ sender.email }}? This cannot be undone!')">
<i class="bi bi-trash"></i> <i class="bi bi-trash"></i>
</button> </button>
</form> </form>

View File

@@ -44,7 +44,7 @@
class="nav-link text-white {{ 'active' if request.endpoint in ['email.senders_list', 'email.add_user'] else '' }}"> class="nav-link text-white {{ 'active' if request.endpoint in ['email.senders_list', 'email.add_user'] else '' }}">
<i class="bi bi-people me-2"></i> <i class="bi bi-people me-2"></i>
Allowed Senders Allowed Senders
<span class="badge bg-secondary ms-auto">{{ user_count if user_count is defined else '' }}</span> <span class="badge bg-secondary ms-auto">{{ sender_count if sender_count is defined else '' }}</span>
</a> </a>
</li> </li>
@@ -122,12 +122,22 @@
<div class="d-flex align-items-center"> <div class="d-flex align-items-center">
<div class="flex-grow-1"> <div class="flex-grow-1">
<small class="text-muted d-block">Server Status</small> <small class="text-muted d-block">Server Status</small>
<small class="text-success"> {% set health = check_health() %}
<small class="{% if health.status == 'healthy' %}text-success{% else %}text-warning{% endif %} status-indicator"
data-bs-toggle="tooltip"
data-bs-html="true"
data-bs-placement="top"
title="{{ (
"<div class='text-start'><strong>Service Status:</strong><br>" +
'SMTP Server: ' + health.services.smtp_server|title + '<br>' +
'Web Frontend: ' + health.services.web_frontend|title + '<br>' +
'Database: ' + health.services.database|title + '</div>'
) | safe }}">
<i class="bi bi-circle-fill me-1" style="font-size: 0.5rem;"></i> <i class="bi bi-circle-fill me-1" style="font-size: 0.5rem;"></i>
Online {{ health.status|title }}
</small> </small>
</div> </div>
<button class="btn btn-outline-secondary btn-sm" title="Refresh Status"> <button class="btn btn-outline-secondary btn-sm" title="Refresh Status" onclick="location.reload()">
<i class="bi bi-arrow-clockwise"></i> <i class="bi bi-arrow-clockwise"></i>
</button> </button>
</div> </div>
@@ -170,6 +180,10 @@
font-size: 0.7rem; font-size: 0.7rem;
} }
.status-indicator {
cursor: pointer;
}
/* Responsive design */ /* Responsive design */
@media (max-width: 768px) { @media (max-width: 768px) {
.sidebar { .sidebar {
@@ -186,3 +200,16 @@
} }
} }
</style> </style>
<script>
document.addEventListener('DOMContentLoaded', function() {
const tooltipTriggerList = document.querySelectorAll('[data-bs-toggle="tooltip"]');
tooltipTriggerList.forEach(function(tooltipTriggerEl) {
new bootstrap.Tooltip(tooltipTriggerEl, {
html: true,
placement: 'top',
trigger: 'hover'
});
});
});
</script>