authentication fix

This commit is contained in:
nahakubuilde
2025-05-31 21:14:05 +01:00
parent aa36a35392
commit 3cd698e289
8 changed files with 465 additions and 113 deletions

2
.gitignore vendored
View File

@@ -12,6 +12,8 @@ test.txt
custom_test.py custom_test.py
custom_test.sh custom_test.sh
.github/ .github/
*.backup
mytest_*
# C extensions # C extensions
*.so *.so

View File

@@ -1,16 +1,33 @@
""" """
Authentication modules for the SMTP server. Enhanced authentication modules for the SMTP server using ESRV schema.
Security Features:
- Users can only send as their own email or domain emails (if permitted)
- IP authentication is domain-specific
- Comprehensive audit logging
- Enhanced validation and error handling
""" """
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 Session, User, Domain, WhitelistedIP, AuthLog, check_password from email_server.models import (
Session, User, Domain, WhitelistedIP,
check_password, log_auth_attempt, get_user_by_email,
get_whitelisted_ip, get_domain_by_name
)
from email_server.tool_box import get_logger from email_server.tool_box import get_logger
logger = get_logger() logger = get_logger()
class Authenticator: class EnhancedAuthenticator:
"""Username/password authenticator.""" """
Enhanced username/password authenticator with sender validation.
Features:
- Validates user credentials
- Stores authenticated user info for sender validation
- Comprehensive audit logging
"""
def __call__(self, server, session, envelope, mechanism, auth_data): def __call__(self, server, session, envelope, mechanism, auth_data):
if not isinstance(auth_data, LoginPassword): if not isinstance(auth_data, LoginPassword):
@@ -25,82 +42,174 @@ class Authenticator:
username = username.decode('utf-8') username = username.decode('utf-8')
if isinstance(password, bytes): if isinstance(password, bytes):
password = password.decode('utf-8') password = password.decode('utf-8')
session_db = Session() peer_ip = session.peer[0]
logger.debug(f'Authentication attempt: {username} from {peer_ip}')
try: try:
peer_ip = session.peer[0]
logger.debug(f'Authentication attempt: {username} from {peer_ip}')
# Look up user in database # Look up user in database
user = session_db.query(User).filter_by(email=username).first() user = get_user_by_email(username)
if user and check_password(password, user.password_hash): if user and check_password(password, user.password_hash):
domain = session_db.query(Domain).filter_by(id=user.domain_id).first() # Store authenticated user info in session for later validation
auth_log = AuthLog( session.authenticated_user = user
timestamp=datetime.now(), session.auth_type = 'user'
peer=str(session.peer),
username=username,
success=True,
message=f'Successful login for {username}'
)
session_db.add(auth_log)
session_db.commit()
logger.debug(f'Authenticated user: {username} from domain {domain.domain_name if domain else "unknown"}') # Log successful authentication
# Don't include the SMTP response code in the message - let aiosmtpd handle it log_auth_attempt(
auth_type='user',
identifier=username,
ip_address=peer_ip,
success=True,
message=f'Successful user authentication'
)
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:
auth_log = AuthLog( # Log failed authentication
timestamp=datetime.now(), log_auth_attempt(
peer=str(session.peer), auth_type='user',
username=username, identifier=username,
ip_address=peer_ip,
success=False, success=False,
message=f'Failed login for {username}: invalid credentials' message=f'Invalid credentials for {username}'
) )
session_db.add(auth_log)
session_db.commit()
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:
session_db.rollback() logger.error(f'Authentication error for {username}: {e}')
logger.error(f'Authentication error: {e}') log_auth_attempt(
auth_type='user',
identifier=username,
ip_address=peer_ip,
success=False,
message=f'Authentication error: {str(e)}'
)
return AuthResult(success=False, handled=True, message='451 Internal server error') return AuthResult(success=False, handled=True, message='451 Internal server error')
finally:
session_db.close()
class IPAuthenticator: class EnhancedIPAuthenticator:
"""IP-based authenticator for clients that don't provide credentials.""" """
Enhanced IP-based authenticator with domain-specific authorization.
def __call__(self, server, session, envelope, mechanism, auth_data): Features:
session_db = Session() - Domain-specific IP authentication
try: - Only allows sending for authorized domain
peer_ip = session.peer[0] - Comprehensive audit logging
logger.debug(f'IP-based authentication attempt from: {peer_ip}') """
# Check if IP is whitelisted def can_authenticate_for_domain(self, ip_address: str, domain_name: str) -> tuple[bool, str]:
whitelist = session_db.query(WhitelistedIP).filter_by(ip_address=peer_ip).first() """
if whitelist: Check if IP can authenticate for a specific domain.
domain = session_db.query(Domain).filter_by(id=whitelist.domain_id).first()
if domain: Args:
auth_log = AuthLog( ip_address: Client IP address
timestamp=datetime.now(), domain_name: Domain to check authorization for
peer=str(session.peer),
username=None,
success=True,
message=f'Authenticated via whitelisted IP for domain {domain.domain_name}'
)
session_db.add(auth_log)
session_db.commit()
logger.debug(f'Authenticated via whitelist: IP {peer_ip} for {domain.domain_name}')
return AuthResult(success=True, handled=True, message='Authenticated via whitelist')
return AuthResult(success=False, handled=True, message='IP not whitelisted') Returns:
(success, message) tuple
"""
try:
whitelisted_ip = get_whitelisted_ip(ip_address, domain_name)
if whitelisted_ip:
return True, f"IP {ip_address} authorized for domain {domain_name}"
else:
return False, f"IP {ip_address} not authorized for domain {domain_name}"
except Exception as e: except Exception as e:
session_db.rollback() logger.error(f"Error checking IP authorization: {e}")
logger.error(f'IP Authentication error: {e}') return False, f"Error checking IP authorization: {str(e)}"
return AuthResult(success=False, handled=True, message='Server error')
finally: def validate_sender_authorization(session, mail_from: str) -> tuple[bool, str]:
session_db.close() """
Validate if the authenticated entity can send as the specified from address.
Args:
session: SMTP session with authentication info
mail_from: The MAIL FROM address
Returns:
(authorized, message) tuple
"""
if not mail_from:
return False, "No sender address provided"
# Extract domain from mail_from
try:
from_domain = mail_from.split('@')[1].lower() if '@' in mail_from else ''
if not from_domain:
return False, "Invalid sender address format"
except (IndexError, AttributeError):
return False, "Invalid sender address format"
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}"
else:
message = f"User {user.email} not authorized to send as {mail_from}"
logger.warning(message)
log_auth_attempt(
auth_type='sender_validation',
identifier=f"{user.email} -> {mail_from}",
ip_address=peer_ip,
success=False,
message=message
)
return False, message
# Check IP authentication for domain
authenticator = EnhancedIPAuthenticator()
can_auth, auth_message = authenticator.can_authenticate_for_domain(peer_ip, from_domain)
if can_auth:
# Store IP auth info in session
session.auth_type = 'ip'
session.authorized_domain = from_domain
log_auth_attempt(
auth_type='ip',
identifier=f"{peer_ip} -> {from_domain}",
ip_address=peer_ip,
success=True,
message=f"IP authorized for domain {from_domain}"
)
logger.info(f"IP {peer_ip} authorized to send for domain {from_domain}")
return True, f"IP authorized for domain {from_domain}"
else:
log_auth_attempt(
auth_type='ip',
identifier=f"{peer_ip} -> {from_domain}",
ip_address=peer_ip,
success=False,
message=auth_message
)
logger.warning(f"IP {peer_ip} not authorized for domain {from_domain}: {auth_message}")
return False, f"Not authorized to send for domain {from_domain}"
def get_authenticated_domain_id(session) -> int:
"""
Get the domain ID for the authenticated entity.
Args:
session: SMTP session with authentication info
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, 'authorized_domain') and session.authorized_domain:
domain = get_domain_by_name(session.authorized_domain)
return domain.id if domain else None
return None

View File

@@ -9,7 +9,7 @@ from email_server.tool_box import get_logger
logger = get_logger() logger = get_logger()
def add_domain(domain_name, requires_auth=True): def add_domain(domain_name):
"""Add a new domain to the database.""" """Add a new domain to the database."""
session = Session() session = Session()
try: try:
@@ -18,7 +18,7 @@ def add_domain(domain_name, requires_auth=True):
print(f"Domain {domain_name} already exists") print(f"Domain {domain_name} already exists")
return False return False
domain = Domain(domain_name=domain_name, requires_auth=requires_auth) domain = Domain(domain_name=domain_name)
session.add(domain) session.add(domain)
session.commit() session.commit()
print(f"Added domain: {domain_name}") print(f"Added domain: {domain_name}")

View File

@@ -13,7 +13,6 @@ import random
import string import string
settings = load_settings() settings = load_settings()
DKIM_SELECTOR = settings['DKIM']['DKIM_SELECTOR']
DKIM_KEY_SIZE = int(settings['DKIM']['DKIM_KEY_SIZE']) DKIM_KEY_SIZE = int(settings['DKIM']['DKIM_KEY_SIZE'])
logger = get_logger() logger = get_logger()

View File

@@ -1,9 +1,15 @@
""" """
Database models for the SMTP server. Database models for the SMTP server using ESRV schema.
Enhanced security features:
- Users can only send as their own email or domain emails (if permitted)
- IP authentication is domain-specific
- All tables use 'esrv_' prefix for namespace isolation
""" """
from sqlalchemy import create_engine, Column, Integer, String, DateTime, Text, Boolean from sqlalchemy import create_engine, Column, Integer, String, DateTime, Text, Boolean
from sqlalchemy.orm import declarative_base, sessionmaker from sqlalchemy.orm import declarative_base, sessionmaker, relationship
from sqlalchemy.sql import func
from datetime import datetime from datetime import datetime
import bcrypt import bcrypt
from email_server.settings_loader import load_settings from email_server.settings_loader import load_settings
@@ -22,72 +28,279 @@ Session = sessionmaker(bind=engine)
logger = get_logger() logger = get_logger()
class Domain(Base): class Domain(Base):
__tablename__ = 'domains' """Domain model with enhanced security features."""
__tablename__ = 'esrv_domains'
id = Column(Integer, primary_key=True) id = Column(Integer, primary_key=True)
domain_name = Column(String, unique=True, nullable=False) domain_name = Column(String, unique=True, nullable=False)
requires_auth = Column(Boolean, default=True) is_active = Column(Boolean, default=True)
created_at = Column(DateTime, default=func.now())
def __repr__(self):
return f"<Domain(id={self.id}, domain_name='{self.domain_name}', active={self.is_active})>"
class User(Base): class User(Base):
__tablename__ = 'users' """
User 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
"""
__tablename__ = 'esrv_users'
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)
password_hash = Column(String, nullable=False) password_hash = Column(String, nullable=False)
domain_id = Column(Integer, nullable=False) domain_id = Column(Integer, nullable=False)
can_send_as_domain = Column(Boolean, default=False)
is_active = Column(Boolean, default=True)
created_at = Column(DateTime, default=func.now())
def can_send_as(self, from_address: str) -> bool:
"""
Check if this user can send emails as the given from_address.
Args:
from_address: The email address the user wants to send from
Returns:
True if user is allowed to send as this address
"""
# User 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 self.can_send_as_domain:
user_domain = self.email.split('@')[1].lower()
from_domain = from_address.split('@')[1].lower() if '@' in from_address else ''
return user_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})>"
class WhitelistedIP(Base): class WhitelistedIP(Base):
__tablename__ = 'whitelisted_ips' """
IP whitelist model with domain-specific authentication.
Security feature:
- IPs can only send emails for their specific authorized domain
"""
__tablename__ = 'esrv_whitelisted_ips'
id = Column(Integer, primary_key=True) id = Column(Integer, primary_key=True)
ip_address = Column(String, unique=True, nullable=False) ip_address = Column(String, nullable=False)
domain_id = Column(Integer, nullable=False) domain_id = Column(Integer, nullable=False)
is_active = Column(Boolean, default=True)
created_at = Column(DateTime, default=func.now())
def can_send_for_domain(self, domain_name: str) -> bool:
"""
Check if this IP can send emails for the given domain.
Args:
domain_name: The domain name to check
Returns:
True if IP is authorized for this domain
"""
if not self.is_active:
return False
# Need to check against the actual domain
session = Session()
try:
domain = session.query(Domain).filter_by(
domain_name=domain_name.lower(),
is_active=True
).first()
return domain and domain.id == self.domain_id
finally:
session.close()
def __repr__(self):
return f"<WhitelistedIP(id={self.id}, ip='{self.ip_address}', domain_id={self.domain_id})>"
class EmailLog(Base): class EmailLog(Base):
__tablename__ = 'email_logs' """Email log model for tracking sent emails."""
__tablename__ = 'esrv_email_logs'
id = Column(Integer, primary_key=True) id = Column(Integer, primary_key=True)
message_id = Column(String, unique=True, nullable=False) from_address = Column(String, nullable=False)
timestamp = Column(DateTime, nullable=False) to_address = Column(String, nullable=False)
peer = Column(String, nullable=False) subject = Column(Text)
mail_from = Column(String, nullable=False)
rcpt_tos = Column(String, nullable=False)
content = Column(Text, nullable=False)
status = Column(String, nullable=False) status = Column(String, nullable=False)
dkim_signed = Column(Boolean, default=False) message = Column(Text)
created_at = Column(DateTime, default=func.now())
def __repr__(self):
return f"<EmailLog(id={self.id}, from='{self.from_address}', to='{self.to_address}', status='{self.status}')>"
class AuthLog(Base): class AuthLog(Base):
__tablename__ = 'auth_logs' """Authentication log model for security auditing."""
__tablename__ = 'esrv_auth_logs'
id = Column(Integer, primary_key=True) id = Column(Integer, primary_key=True)
timestamp = Column(DateTime, nullable=False) auth_type = Column(String, nullable=False) # 'user' or 'ip'
peer = Column(String, nullable=False) identifier = Column(String, nullable=False) # email or IP address
username = Column(String) ip_address = Column(String)
success = Column(Boolean, nullable=False) success = Column(Boolean, nullable=False)
message = Column(String, nullable=False) message = Column(Text)
created_at = Column(DateTime, default=func.now())
def __repr__(self):
return f"<AuthLog(id={self.id}, type='{self.auth_type}', identifier='{self.identifier}', success={self.success})>"
class DKIMKey(Base): class DKIMKey(Base):
__tablename__ = 'dkim_keys' """DKIM key model for email signing."""
__tablename__ = 'esrv_dkim_keys'
id = Column(Integer, primary_key=True) id = Column(Integer, primary_key=True)
domain_id = Column(Integer, nullable=False) domain_id = Column(Integer, nullable=False)
selector = Column(String, nullable=False) selector = Column(String, nullable=False, default='default')
private_key = Column(Text, nullable=False) private_key = Column(Text, nullable=False)
public_key = Column(Text, nullable=False) public_key = Column(Text, nullable=False)
created_at = Column(DateTime, default=datetime.now)
is_active = Column(Boolean, default=True) is_active = Column(Boolean, default=True)
created_at = Column(DateTime, default=func.now())
def __repr__(self):
return f"<DKIMKey(id={self.id}, domain_id={self.domain_id}, selector='{self.selector}', active={self.is_active})>"
class CustomHeader(Base): class CustomHeader(Base):
"""Model for storing custom headers per domain.""" """Custom header model for domain-specific email headers."""
__tablename__ = 'custom_headers' __tablename__ = 'esrv_custom_headers'
id = Column(Integer, primary_key=True) id = Column(Integer, primary_key=True)
domain_id = Column(Integer, nullable=False) domain_id = Column(Integer, nullable=False)
header_name = Column(String, nullable=False) header_name = Column(String, nullable=False)
header_value = Column(String, nullable=False) header_value = Column(String, nullable=False)
is_active = Column(Boolean, default=True) is_active = Column(Boolean, default=True)
created_at = Column(DateTime, default=func.now())
def __repr__(self):
return f"<CustomHeader(id={self.id}, domain_id={self.domain_id}, header='{self.header_name}: {self.header_value}', active={self.is_active})>"
def create_tables(): def create_tables():
"""Create all database tables.""" """Create all database tables using ESRV schema."""
Base.metadata.create_all(engine) Base.metadata.create_all(engine)
logger.info("Created ESRV database tables")
def hash_password(password): def hash_password(password: str) -> str:
"""Hash a password using bcrypt.""" """Hash a password using bcrypt."""
return bcrypt.hashpw(password.encode('utf-8'), bcrypt.gensalt()).decode('utf-8') return bcrypt.hashpw(password.encode('utf-8'), bcrypt.gensalt()).decode('utf-8')
def check_password(password, hashed): def check_password(password: str, hashed: str) -> bool:
"""Check a password against its hash.""" """Check a password against its hash."""
return bcrypt.checkpw(password.encode('utf-8'), hashed.encode('utf-8')) return bcrypt.checkpw(password.encode('utf-8'), hashed.encode('utf-8'))
def log_auth_attempt(auth_type: str, identifier: str, ip_address: str,
success: bool, message: str) -> None:
"""
Log an authentication attempt for security auditing.
Args:
auth_type: Type of auth ('user' or 'ip')
identifier: User email or IP address
ip_address: Client IP address
success: Whether auth was successful
message: Additional details
"""
session = Session()
try:
auth_log = AuthLog(
auth_type=auth_type,
identifier=identifier,
ip_address=ip_address,
success=success,
message=message
)
session.add(auth_log)
session.commit()
logger.info(f"Auth log: {auth_type} {identifier} from {ip_address} - {'SUCCESS' if success else 'FAILED'}")
except Exception as e:
session.rollback()
logger.error(f"Failed to log auth attempt: {e}")
finally:
session.close()
def log_email(from_address: str, to_address: str, subject: str,
status: str, message: str = None) -> None:
"""
Log an email send attempt.
Args:
from_address: Sender email
to_address: Recipient email
subject: Email subject
status: Send status ('sent', 'failed', etc.)
message: Additional details
"""
session = Session()
try:
email_log = EmailLog(
from_address=from_address,
to_address=to_address,
subject=subject,
status=status,
message=message
)
session.add(email_log)
session.commit()
logger.info(f"Email log: {from_address} -> {to_address} - {status}")
except Exception as e:
session.rollback()
logger.error(f"Failed to log email: {e}")
finally:
session.close()
def get_user_by_email(email: str):
"""Get user by email address."""
session = Session()
try:
return session.query(User).filter_by(email=email.lower(), is_active=True).first()
finally:
session.close()
def get_domain_by_name(domain_name: str):
"""Get domain by name."""
session = Session()
try:
return session.query(Domain).filter_by(domain_name=domain_name.lower(), is_active=True).first()
finally:
session.close()
def get_whitelisted_ip(ip_address: str, domain_name: str = None):
"""
Get whitelisted IP, optionally filtered by domain.
Args:
ip_address: IP address to check
domain_name: Optional domain name to restrict to
Returns:
WhitelistedIP object if found and authorized for domain
"""
session = Session()
try:
query = session.query(WhitelistedIP).filter_by(
ip_address=ip_address,
is_active=True
)
if domain_name:
# Join with domain to check authorization
domain = session.query(Domain).filter_by(
domain_name=domain_name.lower(),
is_active=True
).first()
if not domain:
return None
query = query.filter_by(domain_id=domain.id)
return query.first()
finally:
session.close()

View File

@@ -9,7 +9,7 @@ from email_server.tool_box import get_logger
# Import our modules # Import our modules
from email_server.models import create_tables from email_server.models import create_tables
from email_server.smtp_handler import CustomSMTPHandler, PlainController from email_server.smtp_handler import EnhancedCustomSMTPHandler, PlainController
from email_server.tls_utils import generate_self_signed_cert, create_ssl_context from email_server.tls_utils import generate_self_signed_cert, create_ssl_context
from email_server.dkim_manager import DKIMManager from email_server.dkim_manager import DKIMManager
from aiosmtpd.controller import Controller from aiosmtpd.controller import Controller
@@ -52,7 +52,7 @@ async def start_server():
# Add example.com domain if not exists # Add example.com domain if not exists
domain = session.query(Domain).filter_by(domain_name='example.com').first() domain = session.query(Domain).filter_by(domain_name='example.com').first()
if not domain: if not domain:
domain = Domain(domain_name='example.com', requires_auth=True) domain = Domain(domain_name='example.com')
session.add(domain) session.add(domain)
session.commit() session.commit()
logger.debug("Added example.com domain") logger.debug("Added example.com domain")
@@ -95,7 +95,7 @@ async def start_server():
return return
# Start plain SMTP server (with IP whitelist fallback) # Start plain SMTP server (with IP whitelist fallback)
handler_plain = CustomSMTPHandler() handler_plain = EnhancedCustomSMTPHandler()
controller_plain = PlainController( controller_plain = PlainController(
handler_plain, handler_plain,
hostname=BIND_IP, hostname=BIND_IP,
@@ -106,7 +106,7 @@ async def start_server():
logger.debug(f'Starting plain SMTP server on {HOSTNAME}:{SMTP_PORT}...') logger.debug(f'Starting plain SMTP server on {HOSTNAME}:{SMTP_PORT}...')
# Start TLS SMTP server using closure pattern like the original # Start TLS SMTP server using closure pattern like the original
handler_tls = CustomSMTPHandler() handler_tls = EnhancedCustomSMTPHandler()
# Define TLS controller class with ssl_context in closure (like original) # Define TLS controller class with ssl_context in closure (like original)
class TLSController(Controller): class TLSController(Controller):

View File

@@ -31,7 +31,6 @@ DEFAULTS = {
'TLS_KEY_FILE': 'email_server/ssl_certs/server.key', 'TLS_KEY_FILE': 'email_server/ssl_certs/server.key',
}, },
'DKIM': { 'DKIM': {
'DKIM_SELECTOR': 'default',
'DKIM_KEY_SIZE': '2048', 'DKIM_KEY_SIZE': '2048',
}, },
} }

View File

@@ -1,24 +1,37 @@
""" """
SMTP handler for processing incoming emails. Enhanced SMTP handler for processing incoming emails with security controls.
Security Features:
- Users can only send as their own email or domain emails (if permitted)
- IP authentication is domain-specific
- Sender authorization validation
- Enhanced header management
""" """
import uuid import uuid
from datetime import datetime from datetime import datetime
from aiosmtpd.smtp import SMTP as AIOSMTP, AuthResult from aiosmtpd.smtp import SMTP as AIOSMTP, AuthResult
from aiosmtpd.controller import Controller from aiosmtpd.controller import Controller
from email_server.auth import Authenticator, IPAuthenticator from email_server.auth import EnhancedAuthenticator, EnhancedIPAuthenticator, validate_sender_authorization, get_authenticated_domain_id
from email_server.email_relay import EmailRelay from email_server.email_relay import EmailRelay
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
logger = get_logger() logger = get_logger()
class CombinedAuthenticator: class EnhancedCombinedAuthenticator:
"""Combined authenticator that tries username/password first, then falls back to IP whitelist.""" """
Enhanced combined authenticator with sender validation support.
Features:
- User authentication with session storage
- IP-based authentication with domain validation
- Fallback authentication logic
"""
def __init__(self): def __init__(self):
self.user_auth = Authenticator() self.user_auth = EnhancedAuthenticator()
self.ip_auth = IPAuthenticator() self.ip_auth = EnhancedIPAuthenticator()
def __call__(self, server, session, envelope, mechanism, auth_data): def __call__(self, server, session, envelope, mechanism, auth_data):
from aiosmtpd.smtp import LoginPassword from aiosmtpd.smtp import LoginPassword
@@ -31,16 +44,17 @@ class CombinedAuthenticator:
# If user auth fails, don't try IP auth - return the failure # If user auth fails, don't try IP auth - return the failure
return result return result
# If no auth_data provided, try IP-based authentication # If no auth_data provided, IP auth will be validated during MAIL FROM
return self.ip_auth(server, session, envelope, mechanism, auth_data) # For now, allow the connection to proceed
return AuthResult(success=True, handled=True)
class CustomSMTPHandler: class EnhancedCustomSMTPHandler:
"""Custom SMTP handler for processing emails.""" """Enhanced custom SMTP handler with security controls."""
def __init__(self): def __init__(self):
self.authenticator = Authenticator() self.authenticator = EnhancedAuthenticator()
self.ip_authenticator = IPAuthenticator() self.ip_authenticator = EnhancedIPAuthenticator()
self.combined_authenticator = CombinedAuthenticator() self.combined_authenticator = EnhancedCombinedAuthenticator()
self.email_relay = EmailRelay() self.email_relay = EmailRelay()
self.dkim_manager = DKIMManager() self.dkim_manager = DKIMManager()
self.auth_require_tls = False self.auth_require_tls = False
@@ -198,9 +212,25 @@ class CustomSMTPHandler:
return '250 OK' return '250 OK'
async def handle_MAIL(self, server, session, envelope, address, mail_options): async def handle_MAIL(self, server, session, envelope, address, mail_options):
"""Handle MAIL FROM command - validate sender.""" """
Handle MAIL FROM command with enhanced sender validation.
Security Features:
- Validates user can send as the specified address
- Validates IP authorization for domain
- Comprehensive audit logging
"""
logger.debug(f'MAIL FROM: {address}') logger.debug(f'MAIL FROM: {address}')
# Validate sender authorization
authorized, message = validate_sender_authorization(session, address)
if not authorized:
logger.warning(f'MAIL FROM rejected: {address} - {message}')
return f'550 {message}'
envelope.mail_from = address envelope.mail_from = address
logger.info(f'MAIL FROM accepted: {address} - {message}')
return '250 OK' return '250 OK'
class TLSController(Controller): class TLSController(Controller):