fixed layout - rename functions for user to sender
This commit is contained in:
@@ -1,2 +1,2 @@
|
||||
FLASK_APP=app:create_app
|
||||
FLASK_APP=app:flask_app
|
||||
FLASK_ENV=development
|
||||
124
app.py
124
app.py
@@ -31,7 +31,7 @@ sys.path.append(os.path.dirname(os.path.abspath(__file__)))
|
||||
|
||||
# Import SMTP server components
|
||||
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.tool_box import get_logger
|
||||
from email_server.dkim_manager import DKIMManager
|
||||
@@ -91,7 +91,7 @@ class SMTPServerApp:
|
||||
db = SQLAlchemy(app)
|
||||
|
||||
# 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
|
||||
db.Model.metadata = Base.metadata
|
||||
|
||||
@@ -109,71 +109,7 @@ class SMTPServerApp:
|
||||
def index():
|
||||
"""Redirect root to 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
|
||||
@app.errorhandler(404)
|
||||
def not_found_error(error):
|
||||
@@ -192,6 +128,11 @@ class SMTPServerApp:
|
||||
error_message="Internal server error",
|
||||
error_details=str(error)), 500
|
||||
|
||||
@app.route('/health')
|
||||
def health_check():
|
||||
"""Health check endpoint"""
|
||||
return jsonify(self.check_health())
|
||||
|
||||
# Context processors for templates
|
||||
@app.context_processor
|
||||
def utility_processor():
|
||||
@@ -203,6 +144,7 @@ class SMTPServerApp:
|
||||
'zip': zip,
|
||||
'str': str,
|
||||
'int': int,
|
||||
'check_health': self.check_health
|
||||
}
|
||||
|
||||
self.flask_app = app
|
||||
@@ -235,24 +177,24 @@ class SMTPServerApp:
|
||||
for domain_name in sample_domains:
|
||||
dkim_manager.generate_dkim_keypair(domain_name)
|
||||
|
||||
# Add sample users
|
||||
sample_users = [
|
||||
# Add sample senders
|
||||
sample_senders = [
|
||||
('admin@example.com', 'example.com', 'admin123', False),
|
||||
]
|
||||
|
||||
for email, domain_name, password, can_send_as_domain in sample_users:
|
||||
existing = session.query(User).filter_by(email=email).first()
|
||||
for email, domain_name, password, can_send_as_domain in sample_senders:
|
||||
existing = session.query(Sender).filter_by(email=email).first()
|
||||
if not existing:
|
||||
domain = session.query(Domain).filter_by(domain_name=domain_name).first()
|
||||
if domain:
|
||||
user = User(
|
||||
sender = Sender(
|
||||
email=email,
|
||||
password_hash=hash_password(password),
|
||||
domain_id=domain.id,
|
||||
can_send_as_domain=can_send_as_domain
|
||||
)
|
||||
session.add(user)
|
||||
logger.info(f"Added sample user: {email}")
|
||||
session.add(sender)
|
||||
logger.info(f"Added sample sender: {email}")
|
||||
|
||||
# Add sample whitelisted IPs
|
||||
sample_ips = [
|
||||
@@ -374,6 +316,42 @@ class SMTPServerApp:
|
||||
finally:
|
||||
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():
|
||||
"""Main function"""
|
||||
|
||||
@@ -11,8 +11,8 @@ Security Features:
|
||||
from datetime import datetime
|
||||
from aiosmtpd.smtp import AuthResult, LoginPassword
|
||||
from email_server.models import (
|
||||
Session, User, Domain, WhitelistedIP,
|
||||
check_password, log_auth_attempt, get_user_by_email,
|
||||
Session, Sender, Domain, WhitelistedIP,
|
||||
check_password, log_auth_attempt, get_sender_by_email,
|
||||
get_whitelisted_ip, get_domain_by_name
|
||||
)
|
||||
from email_server.tool_box import get_logger
|
||||
@@ -47,42 +47,38 @@ class EnhancedAuthenticator:
|
||||
logger.debug(f'Authentication attempt: {username} from {peer_ip}')
|
||||
|
||||
try:
|
||||
# Look up user in database
|
||||
user = get_user_by_email(username)
|
||||
|
||||
if user and check_password(password, user.password_hash):
|
||||
# Store authenticated user info in session for later validation
|
||||
session.authenticated_user = user
|
||||
session.auth_type = 'user'
|
||||
|
||||
# Look up sender in database
|
||||
sender = get_sender_by_email(username)
|
||||
if sender and check_password(password, sender.password_hash):
|
||||
# Store authenticated sender info in session for later validation
|
||||
session.authenticated_sender = sender
|
||||
session.auth_type = 'sender'
|
||||
# Log successful authentication
|
||||
log_auth_attempt(
|
||||
auth_type='user',
|
||||
auth_type='sender',
|
||||
identifier=username,
|
||||
ip_address=peer_ip,
|
||||
success=True,
|
||||
message=f'Successful user authentication'
|
||||
message=f'Successful sender authentication'
|
||||
)
|
||||
|
||||
logger.info(f'Authenticated user: {username} (ID: {user.id}, can_send_as_domain: {user.can_send_as_domain})')
|
||||
logger.info(f'Authenticated sender: {username} (ID: {sender.id}, can_send_as_domain: {sender.can_send_as_domain})')
|
||||
return AuthResult(success=True, handled=True)
|
||||
else:
|
||||
# Log failed authentication
|
||||
log_auth_attempt(
|
||||
auth_type='user',
|
||||
auth_type='sender',
|
||||
identifier=username,
|
||||
ip_address=peer_ip,
|
||||
success=False,
|
||||
message=f'Invalid credentials for {username}'
|
||||
)
|
||||
|
||||
logger.warning(f'Authentication failed for {username}: invalid credentials')
|
||||
return AuthResult(success=False, handled=True, message='535 Authentication failed')
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f'Authentication error for {username}: {e}')
|
||||
log_auth_attempt(
|
||||
auth_type='user',
|
||||
auth_type='sender',
|
||||
identifier=username,
|
||||
ip_address=peer_ip,
|
||||
success=False,
|
||||
@@ -145,19 +141,18 @@ def validate_sender_authorization(session, mail_from: str) -> tuple[bool, str]:
|
||||
|
||||
peer_ip = session.peer[0]
|
||||
|
||||
# Check user authentication
|
||||
if hasattr(session, 'authenticated_user') and session.authenticated_user:
|
||||
user = session.authenticated_user
|
||||
|
||||
if user.can_send_as(mail_from):
|
||||
logger.info(f"User {user.email} authorized to send as {mail_from}")
|
||||
return True, f"User authorized to send as {mail_from}"
|
||||
# Check sender authentication
|
||||
if hasattr(session, 'authenticated_sender') and session.authenticated_sender:
|
||||
sender = session.authenticated_sender
|
||||
if sender.can_send_as(mail_from):
|
||||
logger.info(f"Sender {sender.email} authorized to send as {mail_from}")
|
||||
return True, f"Sender authorized to send as {mail_from}"
|
||||
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)
|
||||
log_auth_attempt(
|
||||
auth_type='sender_validation',
|
||||
identifier=f"{user.email} -> {mail_from}",
|
||||
identifier=f"{sender.email} -> {mail_from}",
|
||||
ip_address=peer_ip,
|
||||
success=False,
|
||||
message=message
|
||||
@@ -205,8 +200,8 @@ def get_authenticated_domain_id(session) -> int:
|
||||
Returns:
|
||||
Domain ID or None if not authenticated
|
||||
"""
|
||||
if hasattr(session, 'authenticated_user') and session.authenticated_user:
|
||||
return session.authenticated_user.domain_id
|
||||
if hasattr(session, 'authenticated_sender') and session.authenticated_sender:
|
||||
return session.authenticated_sender.domain_id
|
||||
|
||||
if hasattr(session, 'authorized_domain') and session.authorized_domain:
|
||||
domain = get_domain_by_name(session.authorized_domain)
|
||||
|
||||
@@ -38,7 +38,7 @@ class Domain(Base):
|
||||
created_at = Column(DateTime, default=func.now())
|
||||
|
||||
# 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")
|
||||
whitelisted_ips = relationship("WhitelistedIP", backref="domain", lazy="joined")
|
||||
custom_headers = relationship("CustomHeader", backref="domain", lazy="joined")
|
||||
@@ -46,15 +46,15 @@ class Domain(Base):
|
||||
def __repr__(self):
|
||||
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:
|
||||
- can_send_as_domain: If True, user can send as any email from their domain
|
||||
- If False, user can only send as their own email address
|
||||
- can_send_as_domain: If True, sender can send as any email from their domain
|
||||
- If False, sender can only send as their own email address
|
||||
"""
|
||||
__tablename__ = 'esrv_users'
|
||||
__tablename__ = 'esrv_senders'
|
||||
|
||||
id = Column(Integer, primary_key=True)
|
||||
email = Column(String, unique=True, nullable=False)
|
||||
@@ -66,28 +66,25 @@ class User(Base):
|
||||
|
||||
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:
|
||||
from_address: The email address the user wants to send from
|
||||
|
||||
from_address: The email address the sender wants to send from
|
||||
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():
|
||||
return True
|
||||
|
||||
# If user has domain privileges, check if from_address is from same domain
|
||||
# If sender has domain privileges, check if from_address is from same 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 ''
|
||||
return user_domain == from_domain
|
||||
|
||||
return sender_domain == from_domain
|
||||
return False
|
||||
|
||||
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):
|
||||
"""
|
||||
@@ -276,11 +273,11 @@ def log_email(from_address: str, to_address: str, subject: str,
|
||||
finally:
|
||||
session.close()
|
||||
|
||||
def get_user_by_email(email: str):
|
||||
"""Get user by email address."""
|
||||
def get_sender_by_email(email: str):
|
||||
"""Get sender by email address."""
|
||||
session = Session()
|
||||
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:
|
||||
session.close()
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
# 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()
|
||||
try:
|
||||
# Add example.com domain if not exists
|
||||
@@ -57,17 +57,17 @@ async def start_server(shutdown_event=None):
|
||||
session.commit()
|
||||
logger.debug("Added example.com domain")
|
||||
|
||||
# Add test user if not exists
|
||||
user = session.query(User).filter_by(email='test@example.com').first()
|
||||
if not user:
|
||||
user = User(
|
||||
# Add test sender if not exists
|
||||
sender = session.query(Sender).filter_by(email='test@example.com').first()
|
||||
if not sender:
|
||||
sender = Sender(
|
||||
email='test@example.com',
|
||||
password_hash=hash_password('testpass123'),
|
||||
domain_id=domain.id
|
||||
)
|
||||
session.add(user)
|
||||
session.add(sender)
|
||||
session.commit()
|
||||
logger.debug("Added test user: test@example.com")
|
||||
logger.debug("Added test sender: test@example.com")
|
||||
|
||||
# Add whitelisted IP if not exists
|
||||
whitelist = session.query(WhitelistedIP).filter_by(ip_address='127.0.0.1').first()
|
||||
|
||||
@@ -5,7 +5,7 @@ This module provides the main dashboard view and overview functionality.
|
||||
"""
|
||||
|
||||
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 .routes import email_bp
|
||||
|
||||
@@ -19,7 +19,7 @@ def dashboard():
|
||||
try:
|
||||
# Get counts
|
||||
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()
|
||||
|
||||
# Get recent email logs
|
||||
@@ -30,7 +30,7 @@ def dashboard():
|
||||
|
||||
return render_template('dashboard.html',
|
||||
domain_count=domain_count,
|
||||
user_count=user_count,
|
||||
sender_count=sender_count,
|
||||
dkim_count=dkim_count,
|
||||
recent_emails=recent_emails,
|
||||
recent_auths=recent_auths)
|
||||
|
||||
@@ -432,11 +432,22 @@ def check_spf_dns():
|
||||
spf_record = record
|
||||
break
|
||||
|
||||
spf_valid_for_server = False
|
||||
spf_check_message = ''
|
||||
public_ip = get_public_ip()
|
||||
ip_mechanism = f'ip4:{public_ip}'
|
||||
if 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'
|
||||
else:
|
||||
result['success'] = False
|
||||
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)
|
||||
@@ -10,7 +10,7 @@ This module provides domain management functionality including:
|
||||
"""
|
||||
|
||||
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.tool_box import get_logger
|
||||
from sqlalchemy.orm import joinedload
|
||||
@@ -188,12 +188,12 @@ def remove_domain(domain_id: int):
|
||||
domain_name = domain.domain_name
|
||||
|
||||
# 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()
|
||||
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(Sender).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()
|
||||
@@ -202,7 +202,7 @@ def remove_domain(domain_id: int):
|
||||
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')
|
||||
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'))
|
||||
|
||||
except Exception as e:
|
||||
|
||||
@@ -10,27 +10,27 @@ This module provides sender management functionality including:
|
||||
"""
|
||||
|
||||
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
|
||||
import bcrypt
|
||||
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()
|
||||
|
||||
@email_bp.route('/senders')
|
||||
def senders_list():
|
||||
"""List all users."""
|
||||
"""List all senders."""
|
||||
session = Session()
|
||||
try:
|
||||
users = session.query(User, Domain).join(Domain, User.domain_id == Domain.id).order_by(User.email).all()
|
||||
return render_template('senders.html', users=users)
|
||||
senders = session.query(Sender, Domain).join(Domain, Sender.domain_id == Domain.id).order_by(Sender.email).all()
|
||||
return render_template('senders.html', senders=senders)
|
||||
finally:
|
||||
session.close()
|
||||
|
||||
@email_bp.route('/senders/add', methods=['GET', 'POST'])
|
||||
def add_sender():
|
||||
"""Add new user."""
|
||||
"""Add new sender."""
|
||||
session = Session()
|
||||
try:
|
||||
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')
|
||||
return redirect(url_for('email.add_sender'))
|
||||
|
||||
# Check if user already exists
|
||||
existing = session.query(User).filter_by(email=email).first()
|
||||
# Check if sender already exists
|
||||
existing = session.query(Sender).filter_by(email=email).first()
|
||||
if existing:
|
||||
flash(f'User {email} already exists', 'error')
|
||||
flash(f'Sender {email} already exists', 'error')
|
||||
return redirect(url_for('email.senders_list'))
|
||||
|
||||
# Create user
|
||||
user = User(
|
||||
# Create sender
|
||||
sender = Sender(
|
||||
email=email,
|
||||
password_hash=hash_password(password),
|
||||
domain_id=domain_id,
|
||||
can_send_as_domain=can_send_as_domain
|
||||
)
|
||||
session.add(user)
|
||||
session.add(sender)
|
||||
session.commit()
|
||||
|
||||
flash(f'User {email} added successfully', 'success')
|
||||
flash(f'Sender {email} added successfully', 'success')
|
||||
return redirect(url_for('email.senders_list'))
|
||||
|
||||
return render_template('add_sender.html', domains=domains)
|
||||
|
||||
except Exception as e:
|
||||
session.rollback()
|
||||
logger.error(f"Error adding user: {e}")
|
||||
flash(f'Error adding user: {str(e)}', 'error')
|
||||
logger.error(f"Error adding sender: {e}")
|
||||
flash(f'Error adding sender: {str(e)}', 'error')
|
||||
return redirect(url_for('email.add_sender'))
|
||||
finally:
|
||||
session.close()
|
||||
|
||||
@email_bp.route('/senders/<int:user_id>/delete', methods=['POST'])
|
||||
def delete_sender(user_id: int):
|
||||
"""Disable user (soft delete)."""
|
||||
"""Disable sender (soft delete)."""
|
||||
session = Session()
|
||||
try:
|
||||
user = session.query(User).get(user_id)
|
||||
if not user:
|
||||
flash('User not found', 'error')
|
||||
sender = session.query(Sender).get(user_id)
|
||||
if not sender:
|
||||
flash('Sender not found', 'error')
|
||||
return redirect(url_for('email.senders_list'))
|
||||
|
||||
user_email = user.email
|
||||
user.is_active = False
|
||||
sender_email = sender.email
|
||||
sender.is_active = False
|
||||
session.commit()
|
||||
|
||||
flash(f'User {user_email} disabled', 'success')
|
||||
flash(f'Sender {sender_email} disabled', 'success')
|
||||
return redirect(url_for('email.senders_list'))
|
||||
|
||||
except Exception as e:
|
||||
session.rollback()
|
||||
logger.error(f"Error disabling user: {e}")
|
||||
flash(f'Error disabling user: {str(e)}', 'error')
|
||||
logger.error(f"Error disabling sender: {e}")
|
||||
flash(f'Error disabling sender: {str(e)}', 'error')
|
||||
return redirect(url_for('email.senders_list'))
|
||||
finally:
|
||||
session.close()
|
||||
|
||||
@email_bp.route('/senders/<int:user_id>/enable', methods=['POST'])
|
||||
def enable_sender(user_id: int):
|
||||
"""Enable user."""
|
||||
"""Enable sender."""
|
||||
session = Session()
|
||||
try:
|
||||
user = session.query(User).get(user_id)
|
||||
if not user:
|
||||
flash('User not found', 'error')
|
||||
sender = session.query(Sender).get(user_id)
|
||||
if not sender:
|
||||
flash('Sender not found', 'error')
|
||||
return redirect(url_for('email.senders_list'))
|
||||
|
||||
user_email = user.email
|
||||
user.is_active = True
|
||||
sender_email = sender.email
|
||||
sender.is_active = True
|
||||
session.commit()
|
||||
|
||||
flash(f'User {user_email} enabled', 'success')
|
||||
flash(f'Sender {sender_email} enabled', 'success')
|
||||
return redirect(url_for('email.senders_list'))
|
||||
|
||||
except Exception as e:
|
||||
session.rollback()
|
||||
logger.error(f"Error enabling user: {e}")
|
||||
flash(f'Error enabling user: {str(e)}', 'error')
|
||||
logger.error(f"Error enabling sender: {e}")
|
||||
flash(f'Error enabling sender: {str(e)}', 'error')
|
||||
return redirect(url_for('email.senders_list'))
|
||||
finally:
|
||||
session.close()
|
||||
|
||||
@email_bp.route('/senders/<int:user_id>/remove', methods=['POST'])
|
||||
def remove_sender(user_id: int):
|
||||
"""Permanently remove user."""
|
||||
"""Permanently remove sender."""
|
||||
session = Session()
|
||||
try:
|
||||
user = session.query(User).get(user_id)
|
||||
if not user:
|
||||
flash('User not found', 'error')
|
||||
sender = session.query(Sender).get(user_id)
|
||||
if not sender:
|
||||
flash('Sender not found', 'error')
|
||||
return redirect(url_for('email.senders_list'))
|
||||
|
||||
user_email = user.email
|
||||
session.delete(user)
|
||||
sender_email = sender.email
|
||||
session.delete(sender)
|
||||
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'))
|
||||
|
||||
except Exception as e:
|
||||
session.rollback()
|
||||
logger.error(f"Error removing user: {e}")
|
||||
flash(f'Error removing user: {str(e)}', 'error')
|
||||
logger.error(f"Error removing sender: {e}")
|
||||
flash(f'Error removing sender: {str(e)}', 'error')
|
||||
return redirect(url_for('email.senders_list'))
|
||||
finally:
|
||||
session.close()
|
||||
|
||||
@email_bp.route('/senders/<int:user_id>/edit', methods=['GET', 'POST'])
|
||||
def edit_sender(user_id: int):
|
||||
"""Edit user."""
|
||||
"""Edit sender."""
|
||||
session = Session()
|
||||
try:
|
||||
user = session.query(User).get(user_id)
|
||||
if not user:
|
||||
flash('User not found', 'error')
|
||||
sender = session.query(Sender).get(user_id)
|
||||
if not sender:
|
||||
flash('Sender not found', 'error')
|
||||
return redirect(url_for('email.senders_list'))
|
||||
|
||||
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')
|
||||
return redirect(url_for('email.edit_sender', user_id=user_id))
|
||||
|
||||
# Check if email already exists (excluding current user)
|
||||
existing = session.query(User).filter(
|
||||
User.email == email,
|
||||
User.id != user_id
|
||||
# Check if email already exists (excluding current sender)
|
||||
existing = session.query(Sender).filter(
|
||||
Sender.email == email,
|
||||
Sender.id != user_id
|
||||
).first()
|
||||
if existing:
|
||||
flash(f'Email {email} already exists', 'error')
|
||||
return redirect(url_for('email.edit_sender', user_id=user_id))
|
||||
|
||||
# Update user
|
||||
user.email = email
|
||||
user.domain_id = domain_id
|
||||
user.can_send_as_domain = can_send_as_domain
|
||||
# Update sender
|
||||
sender.email = email
|
||||
sender.domain_id = domain_id
|
||||
sender.can_send_as_domain = can_send_as_domain
|
||||
|
||||
# Update password if provided
|
||||
if password:
|
||||
user.password_hash = hash_password(password)
|
||||
sender.password_hash = hash_password(password)
|
||||
|
||||
session.commit()
|
||||
|
||||
flash(f'User {email} updated successfully', 'success')
|
||||
flash(f'Sender {email} updated successfully', 'success')
|
||||
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:
|
||||
session.rollback()
|
||||
logger.error(f"Error editing user: {e}")
|
||||
flash(f'Error editing user: {str(e)}', 'error')
|
||||
logger.error(f"Error editing sender: {e}")
|
||||
flash(f'Error editing sender: {str(e)}', 'error')
|
||||
return redirect(url_for('email.senders_list'))
|
||||
finally:
|
||||
session.close()
|
||||
@@ -131,6 +131,20 @@
|
||||
<!-- Custom SMTP Management CSS -->
|
||||
<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 %}
|
||||
</head>
|
||||
<body>
|
||||
@@ -337,6 +351,16 @@
|
||||
<!-- Custom SMTP Management JavaScript -->
|
||||
<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 %}
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
<div class="row">
|
||||
<!-- Statistics Cards -->
|
||||
<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-body">
|
||||
<div class="d-flex align-items-center">
|
||||
@@ -24,19 +25,21 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<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-body">
|
||||
<div class="d-flex align-items-center">
|
||||
<div class="flex-grow-1">
|
||||
<h5 class="card-title text-success mb-1">
|
||||
<i class="bi bi-people me-2"></i>
|
||||
Users
|
||||
Senders
|
||||
</h5>
|
||||
<h3 class="mb-0">{{ user_count }}</h3>
|
||||
<small class="text-muted">Authenticated users</small>
|
||||
<h3 class="mb-0">{{ sender_count }}</h3>
|
||||
<small class="text-muted">Authenticated senders</small>
|
||||
</div>
|
||||
<div class="fs-2 text-success opacity-50">
|
||||
<i class="bi bi-people"></i>
|
||||
@@ -44,9 +47,11 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<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-body">
|
||||
<div class="d-flex align-items-center">
|
||||
@@ -64,6 +69,7 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div class="col-lg-3 col-md-6 mb-4">
|
||||
@@ -75,11 +81,28 @@
|
||||
<i class="bi bi-activity me-2"></i>
|
||||
Status
|
||||
</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>
|
||||
Online
|
||||
{{ health.status|title }}
|
||||
</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 class="fs-2 text-info opacity-50">
|
||||
<i class="bi bi-activity"></i>
|
||||
@@ -248,7 +271,7 @@
|
||||
<div class="d-grid">
|
||||
<a href="{{ url_for('email.add_sender') }}" class="btn btn-outline-success">
|
||||
<i class="bi bi-person-plus me-2"></i>
|
||||
Add User
|
||||
Add Sender
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
@@ -282,4 +305,31 @@
|
||||
location.reload();
|
||||
}, 30000);
|
||||
</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 %}
|
||||
|
||||
@@ -331,6 +331,30 @@
|
||||
|
||||
{% block extra_js %}
|
||||
<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
|
||||
function showToast(message, type = 'info') {
|
||||
const toastContainer = document.getElementById('toastContainer');
|
||||
@@ -433,6 +457,14 @@
|
||||
// Update SPF status
|
||||
if (spfResult.success) {
|
||||
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 {
|
||||
spfStatus.innerHTML = '<span class="status-indicator status-danger"></span><small class="text-danger">✗ Not found</small>';
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block title %}Edit User - SMTP Management{% endblock %}
|
||||
{% block title %}Edit Sender - SMTP Management{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="row">
|
||||
@@ -9,7 +9,7 @@
|
||||
<div class="card-header">
|
||||
<h5 class="mb-0">
|
||||
<i class="bi bi-person-fill-gear me-2"></i>
|
||||
Edit User
|
||||
Edit Sender
|
||||
</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
@@ -20,7 +20,7 @@
|
||||
class="form-control"
|
||||
id="email"
|
||||
name="email"
|
||||
value="{{ user.email }}"
|
||||
value="{{ sender.email }}"
|
||||
required>
|
||||
</div>
|
||||
|
||||
@@ -42,7 +42,7 @@
|
||||
<option value="">Select a domain</option>
|
||||
{% for domain in domains %}
|
||||
<option value="{{ domain.id }}"
|
||||
{% if domain.id == user.domain_id %}selected{% endif %}>
|
||||
{% if domain.id == sender.domain_id %}selected{% endif %}>
|
||||
{{ domain.domain_name }}
|
||||
</option>
|
||||
{% endfor %}
|
||||
@@ -55,12 +55,12 @@
|
||||
type="checkbox"
|
||||
id="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">
|
||||
<strong>Can send as any email from domain</strong>
|
||||
</label>
|
||||
<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>
|
||||
@@ -68,7 +68,7 @@
|
||||
<div class="d-flex gap-2">
|
||||
<button type="submit" class="btn btn-primary">
|
||||
<i class="bi bi-check-lg me-1"></i>
|
||||
Update User
|
||||
Update Sender
|
||||
</button>
|
||||
<a href="{{ url_for('email.senders_list') }}" class="btn btn-secondary">
|
||||
<i class="bi bi-x-lg me-1"></i>
|
||||
@@ -85,26 +85,26 @@
|
||||
<div class="card-header">
|
||||
<h6 class="mb-0">
|
||||
<i class="bi bi-info-circle me-2"></i>
|
||||
Current User Details
|
||||
Current Sender Details
|
||||
</h6>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<dl class="row mb-0">
|
||||
<dt class="col-sm-4">Email:</dt>
|
||||
<dd class="col-sm-8">
|
||||
<code>{{ user.email }}</code>
|
||||
<code>{{ sender.email }}</code>
|
||||
</dd>
|
||||
<dt class="col-sm-4">Domain:</dt>
|
||||
<dd class="col-sm-8">
|
||||
{% for domain in domains %}
|
||||
{% if domain.id == user.domain_id %}
|
||||
{% if domain.id == sender.domain_id %}
|
||||
<span class="badge bg-secondary">{{ domain.domain_name }}</span>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</dd>
|
||||
<dt class="col-sm-4">Status:</dt>
|
||||
<dd class="col-sm-8">
|
||||
{% if user.is_active %}
|
||||
{% if sender.is_active %}
|
||||
<span class="badge bg-success">
|
||||
<i class="bi bi-check-circle me-1"></i>
|
||||
Active
|
||||
@@ -118,7 +118,7 @@
|
||||
</dd>
|
||||
<dt class="col-sm-4">Domain Sender:</dt>
|
||||
<dd class="col-sm-8">
|
||||
{% if user.can_send_as_domain %}
|
||||
{% if sender.can_send_as_domain %}
|
||||
<span class="badge bg-success">
|
||||
<i class="bi bi-check-circle me-1"></i>
|
||||
Yes
|
||||
@@ -133,7 +133,7 @@
|
||||
<dt class="col-sm-4">Created:</dt>
|
||||
<dd class="col-sm-8">
|
||||
<small class="text-muted">
|
||||
{{ user.created_at.strftime('%Y-%m-%d %H:%M') }}
|
||||
{{ sender.created_at.strftime('%Y-%m-%d %H:%M') }}
|
||||
</small>
|
||||
</dd>
|
||||
</dl>
|
||||
@@ -144,7 +144,7 @@
|
||||
<div class="card-header">
|
||||
<h6 class="mb-0">
|
||||
<i class="bi bi-shield-check me-2"></i>
|
||||
User Permissions
|
||||
Sender Permissions
|
||||
</h6>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
@@ -154,8 +154,8 @@
|
||||
Domain Sender Permission
|
||||
</h6>
|
||||
<ul class="mb-0 small">
|
||||
<li><strong>Enabled:</strong> User can send emails using any address in their domain (e.g., admin@domain.com, support@domain.com)</li>
|
||||
<li><strong>Disabled:</strong> User can only send emails from their own email address</li>
|
||||
<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> Sender can only send emails from their own email address</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -76,7 +76,7 @@
|
||||
</h5>
|
||||
</div>
|
||||
<div class="card-body p-0">
|
||||
{% if users %}
|
||||
{% if senders %}
|
||||
<div class="table-responsive">
|
||||
<table class="table table-dark table-hover mb-0">
|
||||
<thead>
|
||||
@@ -90,16 +90,16 @@
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for user, domain in users %}
|
||||
{% for sender, domain in senders %}
|
||||
<tr>
|
||||
<td>
|
||||
<div class="fw-bold">{{ user.email }}</div>
|
||||
<div class="fw-bold">{{ sender.email }}</div>
|
||||
</td>
|
||||
<td>
|
||||
<span class="badge bg-secondary">{{ domain.domain_name }}</span>
|
||||
</td>
|
||||
<td>
|
||||
{% if user.can_send_as_domain %}
|
||||
{% if sender.can_send_as_domain %}
|
||||
<span class="badge bg-warning" style="color: black;">
|
||||
<i class="bi bi-star me-1"></i>
|
||||
Domain Sender
|
||||
@@ -112,11 +112,11 @@
|
||||
Regular Sender
|
||||
</span>
|
||||
<br>
|
||||
<small class="text-muted">Can only send as {{ user.email }}</small>
|
||||
<small class="text-muted">Can only send as {{ sender.email }}</small>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>
|
||||
{% if user.is_active %}
|
||||
{% if sender.is_active %}
|
||||
<span class="badge bg-success">
|
||||
<i class="bi bi-check-circle me-1"></i>
|
||||
Active
|
||||
@@ -130,45 +130,45 @@
|
||||
</td>
|
||||
<td>
|
||||
<small class="text-muted">
|
||||
{{ user.created_at.strftime('%Y-%m-%d %H:%M') }}
|
||||
{{ sender.created_at.strftime('%Y-%m-%d %H:%M') }}
|
||||
</small>
|
||||
</td>
|
||||
<td>
|
||||
<div class="btn-group" role="group">
|
||||
<!-- Edit Button -->
|
||||
<a href="{{ url_for('email.edit_sender', user_id=user.id) }}"
|
||||
<a href="{{ url_for('email.edit_sender', user_id=sender.id) }}"
|
||||
class="btn btn-outline-primary btn-sm"
|
||||
title="Edit Sender">
|
||||
<i class="bi bi-pencil"></i>
|
||||
</a>
|
||||
|
||||
<!-- Enable/Disable Button -->
|
||||
{% if user.is_active %}
|
||||
<form method="post" action="{{ url_for('email.delete_sender', user_id=user.id) }}" class="d-inline">
|
||||
{% if sender.is_active %}
|
||||
<form method="post" action="{{ url_for('email.delete_sender', user_id=sender.id) }}" class="d-inline">
|
||||
<button type="submit"
|
||||
class="btn btn-outline-warning btn-sm"
|
||||
title="Disable Sender"
|
||||
onclick="return confirm('Disable user {{ user.email }}?')">
|
||||
onclick="return confirm('Disable user {{ sender.email }}?')">
|
||||
<i class="bi bi-pause-circle"></i>
|
||||
</button>
|
||||
</form>
|
||||
{% 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"
|
||||
class="btn btn-outline-success btn-sm"
|
||||
title="Enable Sender"
|
||||
onclick="return confirm('Enable user {{ user.email }}?')">
|
||||
onclick="return confirm('Enable user {{ sender.email }}?')">
|
||||
<i class="bi bi-play-circle"></i>
|
||||
</button>
|
||||
</form>
|
||||
{% endif %}
|
||||
|
||||
<!-- 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"
|
||||
class="btn btn-outline-danger btn-sm"
|
||||
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>
|
||||
</button>
|
||||
</form>
|
||||
|
||||
@@ -44,7 +44,7 @@
|
||||
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>
|
||||
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>
|
||||
</li>
|
||||
|
||||
@@ -122,12 +122,22 @@
|
||||
<div class="d-flex align-items-center">
|
||||
<div class="flex-grow-1">
|
||||
<small class="text-muted d-block">Server Status</small>
|
||||
<small class="text-success">
|
||||
{% 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>
|
||||
Online
|
||||
{{ health.status|title }}
|
||||
</small>
|
||||
</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>
|
||||
</button>
|
||||
</div>
|
||||
@@ -170,6 +180,10 @@
|
||||
font-size: 0.7rem;
|
||||
}
|
||||
|
||||
.status-indicator {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
/* Responsive design */
|
||||
@media (max-width: 768px) {
|
||||
.sidebar {
|
||||
@@ -186,3 +200,16 @@
|
||||
}
|
||||
}
|
||||
</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>
|
||||
|
||||
Reference in New Issue
Block a user