checking encryption, fixing module calling
This commit is contained in:
@@ -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
|
||||
"""
|
||||
@@ -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__)
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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__)
|
||||
|
||||
|
||||
@@ -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()
|
||||
@@ -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()
|
||||
|
||||
@@ -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.')
|
||||
@@ -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,
|
||||
|
||||
@@ -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
13
email_server/tool_box.py
Normal 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)
|
||||
Reference in New Issue
Block a user