checking encryption, fixing module calling

This commit is contained in:
nahakubuilde
2025-05-30 22:01:48 +01:00
parent aa7285af39
commit 7e55bfed58
20 changed files with 172 additions and 67 deletions

View File

@@ -1,9 +1,3 @@
from .auth import *
from .dkim_manager import *
from .server_runner import *
from .tls_utils import *
from .cli_tools import *
from .email_relay import *
from .models import *
from .smtp_handler import *
from .config import *
"""
PyMTA Server email package
"""

View File

@@ -5,7 +5,7 @@ Authentication modules for the SMTP server.
import logging
from datetime import datetime
from aiosmtpd.smtp import AuthResult, LoginPassword
from .models import Session, User, Domain, WhitelistedIP, AuthLog, check_password
from email_server.models import Session, User, Domain, WhitelistedIP, AuthLog, check_password
logger = logging.getLogger(__name__)

View File

@@ -3,9 +3,8 @@ Command-line tools for managing the SMTP server.
"""
import argparse
import sys
from .models import Session, Domain, User, WhitelistedIP, hash_password, create_tables
from .dkim_manager import DKIMManager
from email_server.models import Session, Domain, User, WhitelistedIP, hash_password, create_tables
from email_server.dkim_manager import DKIMManager
import logging
logging.basicConfig(level=logging.INFO)

View File

@@ -6,6 +6,7 @@ Configuration settings for the SMTP server.
SMTP_PORT = 4025
SMTP_TLS_PORT = 40587
HOSTNAME = 'localhost'
BIND_IP = '0.0.0.0'
# Database settings
DATABASE_URL = 'sqlite:///email_server/server_data/smtp_server.db'

View File

@@ -7,8 +7,8 @@ import dkim
from cryptography.hazmat.primitives import serialization, hashes
from cryptography.hazmat.primitives.asymmetric import rsa
from datetime import datetime
from .models import Session, Domain, DKIMKey
from .config import DKIM_SELECTOR, DKIM_KEY_SIZE
from email_server.models import Session, Domain, DKIMKey
from email_server.config import DKIM_SELECTOR, DKIM_KEY_SIZE
logger = logging.getLogger(__name__)

View File

@@ -4,9 +4,10 @@ Email relay functionality for the SMTP server.
import dns.resolver
import smtplib
import ssl
import logging
from datetime import datetime
from .models import Session, EmailLog
from email_server.models import Session, EmailLog
logger = logging.getLogger(__name__)
@@ -14,33 +15,112 @@ class EmailRelay:
"""Handles relaying emails to recipient mail servers."""
def __init__(self):
self.timeout = 10
self.timeout = 30 # Increased timeout for TLS negotiations
def relay_email(self, mail_from, rcpt_tos, content):
"""Relay email to recipient's mail server."""
"""Relay email to recipient's mail server with opportunistic TLS."""
try:
for rcpt in rcpt_tos:
domain = rcpt.split('@')[1]
# Resolve MX record for the domain
try:
mx_records = dns.resolver.resolve(domain, 'MX')
# Sort by priority (lower number = higher priority)
mx_records = sorted(mx_records, key=lambda x: x.preference)
mx_host = mx_records[0].exchange.to_text().rstrip('.')
logger.info(f'Found MX record for {domain}: {mx_host}')
except Exception as e:
logger.error(f'Failed to resolve MX for {domain}: {e}')
return False
try:
with smtplib.SMTP(mx_host, 25, timeout=self.timeout) as relay_server:
relay_server.set_debuglevel(1)
relay_server.sendmail(mail_from, rcpt, content)
logger.info(f'Relayed email to {rcpt} via {mx_host}')
except Exception as e:
logger.error(f'Failed to relay email to {rcpt}: {e}')
# Try to relay with opportunistic TLS
if not self._relay_with_opportunistic_tls(mail_from, rcpt, content, mx_host):
return False
return True
except Exception as e:
logger.error(f'General relay error: {e}')
return False
def _relay_with_opportunistic_tls(self, mail_from, rcpt, content, mx_host):
"""Relay email with opportunistic TLS (like Gmail does)."""
try:
# First, try with STARTTLS (encrypted)
try:
with smtplib.SMTP(mx_host, 25, timeout=self.timeout) as relay_server:
relay_server.set_debuglevel(1)
# Try to enable TLS if the server supports it
try:
# Check if server supports STARTTLS
relay_server.ehlo()
if relay_server.has_extn('starttls'):
logger.info(f'Starting TLS connection to {mx_host}')
context = ssl.create_default_context()
# Allow self-signed certificates for mail servers
context.check_hostname = False
context.verify_mode = ssl.CERT_NONE
relay_server.starttls(context=context)
relay_server.ehlo() # Say hello again after STARTTLS
logger.info(f'TLS connection established to {mx_host}')
else:
logger.warning(f'Server {mx_host} does not support STARTTLS, using plain text')
except Exception as tls_e:
logger.warning(f'STARTTLS failed with {mx_host}, continuing with plain text: {tls_e}')
# Send the email
relay_server.sendmail(mail_from, rcpt, content)
logger.info(f'Successfully relayed email to {rcpt} via {mx_host}')
return True
except Exception as e:
logger.error(f'Failed to relay email to {rcpt} via {mx_host}: {e}')
# Fallback: try alternative MX records if available
try:
domain = rcpt.split('@')[1]
mx_records = dns.resolver.resolve(domain, 'MX')
mx_records = sorted(mx_records, key=lambda x: x.preference)
# Try other MX records
for mx_record in mx_records[1:3]: # Try up to 2 backup MX records
backup_mx = mx_record.exchange.to_text().rstrip('.')
logger.info(f'Trying backup MX record: {backup_mx}')
try:
with smtplib.SMTP(backup_mx, 25, timeout=self.timeout) as backup_server:
backup_server.set_debuglevel(1)
# Try TLS with backup server too
try:
backup_server.ehlo()
if backup_server.has_extn('starttls'):
context = ssl.create_default_context()
context.check_hostname = False
context.verify_mode = ssl.CERT_NONE
backup_server.starttls(context=context)
backup_server.ehlo()
logger.info(f'TLS connection established to backup {backup_mx}')
except Exception:
logger.warning(f'STARTTLS failed with backup {backup_mx}, using plain text')
backup_server.sendmail(mail_from, rcpt, content)
logger.info(f'Successfully relayed email to {rcpt} via backup {backup_mx}')
return True
except Exception as backup_e:
logger.warning(f'Backup MX {backup_mx} also failed: {backup_e}')
continue
except Exception as fallback_e:
logger.error(f'All MX records failed for {rcpt}: {fallback_e}')
return False
except Exception as e:
logger.error(f'Unexpected error in TLS relay: {e}')
return False
def log_email(self, message_id, peer, mail_from, rcpt_tos, content, status, dkim_signed=False):
"""Log email activity to database."""
session_db = Session()
@@ -68,4 +148,4 @@ class EmailRelay:
session_db.rollback()
logger.error(f'Error logging email: {e}')
finally:
session_db.close()
session_db.close()

View File

@@ -6,7 +6,10 @@ from sqlalchemy import create_engine, Column, Integer, String, DateTime, Text, B
from sqlalchemy.orm import declarative_base, sessionmaker
from datetime import datetime
import bcrypt
from .config import DATABASE_URL
from email_server.config import DATABASE_URL
from email_server.tool_box import ensure_folder_exists
ensure_folder_exists(DATABASE_URL)
# SQLAlchemy setup
Base = declarative_base()

View File

@@ -5,15 +5,13 @@ Main server file that ties all modules together.
import asyncio
import logging
import sys
import os
# Import our modules
from .config import SMTP_PORT, SMTP_TLS_PORT, HOSTNAME, LOG_LEVEL
from .models import create_tables
from .smtp_handler import CustomSMTPHandler, PlainController
from .tls_utils import generate_self_signed_cert, create_ssl_context
from .dkim_manager import DKIMManager
from email_server.config import SMTP_PORT, SMTP_TLS_PORT, HOSTNAME, LOG_LEVEL, BIND_IP
from email_server.models import create_tables
from email_server.smtp_handler import CustomSMTPHandler, PlainController
from email_server.tls_utils import generate_self_signed_cert, create_ssl_context
from email_server.dkim_manager import DKIMManager
from aiosmtpd.controller import Controller
from aiosmtpd.smtp import SMTP as AIOSMTP
@@ -98,7 +96,8 @@ async def start_server():
handler_plain = CustomSMTPHandler()
controller_plain = PlainController(
handler_plain,
hostname=HOSTNAME,
hostname=BIND_IP,
server_hostname="TestEnvironment",
port=SMTP_PORT
)
controller_plain.start()
@@ -122,21 +121,15 @@ async def start_server():
controller_tls = TLSController(
handler_tls,
hostname=HOSTNAME,
hostname=BIND_IP,
server_hostname="TestEnvironment",
port=SMTP_TLS_PORT
)
controller_tls.start()
logger.info(f'Starting STARTTLS SMTP server on {HOSTNAME}:{SMTP_TLS_PORT}...')
logger.info('Both SMTP servers are running:')
logger.info(f' - Plain SMTP (IP whitelist): {HOSTNAME}:{SMTP_PORT}')
logger.info(f' - STARTTLS SMTP (auth required): {HOSTNAME}:{SMTP_TLS_PORT}')
logger.info(' - DKIM signing enabled for configured domains')
logger.info('')
logger.info(f' - Plain SMTP (IP whitelist): {BIND_IP}:{SMTP_PORT}')
logger.info(f' - STARTTLS SMTP (auth required): {BIND_IP}:{SMTP_TLS_PORT}')
logger.info('Management commands:')
logger.info(' python cli_tools.py --help')
logger.info('')
logger.info('Press Ctrl+C to stop the servers...')
try:
await asyncio.Event().wait()
@@ -144,14 +137,4 @@ async def start_server():
logger.info('Shutting down SMTP servers...')
controller_plain.stop()
controller_tls.stop()
logger.info('SMTP servers stopped.')
if __name__ == '__main__':
try:
asyncio.run(start_server())
except KeyboardInterrupt:
logger.info('Server interrupted by user')
sys.exit(0)
except Exception as e:
logger.error(f'Server error: {e}')
sys.exit(1)
logger.info('SMTP servers stopped.')

View File

@@ -7,9 +7,9 @@ import uuid
from datetime import datetime
from aiosmtpd.smtp import SMTP as AIOSMTP, AuthResult
from aiosmtpd.controller import Controller
from .auth import Authenticator, IPAuthenticator
from .email_relay import EmailRelay
from .dkim_manager import DKIMManager
from email_server.auth import Authenticator, IPAuthenticator
from email_server.email_relay import EmailRelay
from email_server.dkim_manager import DKIMManager
logger = logging.getLogger(__name__)
@@ -124,7 +124,7 @@ class TLSController(Controller):
return AIOSMTP(
self.handler,
tls_context=self.ssl_context,
require_starttls=False, # Don't force STARTTLS, but make it available
require_starttls=True, # Don't force STARTTLS, but make it available
auth_require_tls=True, # If auth is used, require TLS
authenticator=self.handler.combined_authenticator,
decode_data=True,

View File

@@ -6,10 +6,14 @@ import ssl
import os
import logging
from OpenSSL import crypto
from .config import TLS_CERT_FILE, TLS_KEY_FILE
from email_server.config import TLS_CERT_FILE, TLS_KEY_FILE
from email_server.tool_box import ensure_folder_exists
logger = logging.getLogger(__name__)
ensure_folder_exists(TLS_CERT_FILE)
ensure_folder_exists(TLS_KEY_FILE)
def generate_self_signed_cert():
"""Generate self-signed SSL certificate if it doesn't exist."""
if os.path.exists(TLS_CERT_FILE) and os.path.exists(TLS_KEY_FILE):
@@ -26,6 +30,8 @@ def generate_self_signed_cert():
# Generate certificate
cert = crypto.X509()
cert.get_subject().CN = 'localhost'
cert.get_subject().O = 'PyMTA Server'
cert.get_subject().C = 'GB'
cert.set_serial_number(1000)
cert.gmtime_adj_notBefore(0)
cert.gmtime_adj_notAfter(365 * 24 * 60 * 60) # Valid for 1 year

13
email_server/tool_box.py Normal file
View File

@@ -0,0 +1,13 @@
"""
Utility functions for the email server.
"""
import os
def ensure_folder_exists(filepath):
"""
Ensure that the folder for the given filepath exists.
"""
if filepath.startswith("sqlite:///"):
filepath = filepath.replace("sqlite:///", "", 1)
os.makedirs(os.path.dirname(filepath), exist_ok=True)