updated config generation
This commit is contained in:
4
.gitignore
vendored
4
.gitignore
vendored
@@ -4,10 +4,14 @@ __pycache__/
|
||||
*$py.class
|
||||
|
||||
# Certs, db, private test files
|
||||
settings.ini
|
||||
*.crt
|
||||
*.key
|
||||
*.db
|
||||
test.txt
|
||||
custom_test.py
|
||||
custom_test.sh
|
||||
.github/
|
||||
|
||||
# C extensions
|
||||
*.so
|
||||
|
||||
@@ -2,12 +2,12 @@
|
||||
Authentication modules for the SMTP server.
|
||||
"""
|
||||
|
||||
import logging
|
||||
from datetime import datetime
|
||||
from aiosmtpd.smtp import AuthResult, LoginPassword
|
||||
from email_server.models import Session, User, Domain, WhitelistedIP, AuthLog, check_password
|
||||
from email_server.tool_box import get_logger
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
logger = get_logger()
|
||||
|
||||
class Authenticator:
|
||||
"""Username/password authenticator."""
|
||||
@@ -47,7 +47,7 @@ class Authenticator:
|
||||
session_db.add(auth_log)
|
||||
session_db.commit()
|
||||
|
||||
logger.info(f'Authenticated user: {username} from domain {domain.domain_name if domain else "unknown"}')
|
||||
logger.debug(f'Authenticated user: {username} from domain {domain.domain_name if domain else "unknown"}')
|
||||
# Don't include the SMTP response code in the message - let aiosmtpd handle it
|
||||
return AuthResult(success=True, handled=True)
|
||||
else:
|
||||
@@ -94,7 +94,7 @@ class IPAuthenticator:
|
||||
)
|
||||
session_db.add(auth_log)
|
||||
session_db.commit()
|
||||
logger.info(f'Authenticated via whitelist: IP {peer_ip} for {domain.domain_name}')
|
||||
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')
|
||||
|
||||
@@ -5,10 +5,9 @@ Command-line tools for managing the SMTP server.
|
||||
import argparse
|
||||
from email_server.models import Session, Domain, User, WhitelistedIP, hash_password, create_tables
|
||||
from email_server.dkim_manager import DKIMManager
|
||||
import logging
|
||||
from email_server.tool_box import get_logger
|
||||
|
||||
logging.basicConfig(level=logging.INFO)
|
||||
logger = logging.getLogger(__name__)
|
||||
logger = get_logger()
|
||||
|
||||
def add_domain(domain_name, requires_auth=True):
|
||||
"""Add a new domain to the database."""
|
||||
|
||||
@@ -1,26 +0,0 @@
|
||||
"""
|
||||
Configuration settings for the SMTP server.
|
||||
"""
|
||||
|
||||
# Server settings
|
||||
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'
|
||||
|
||||
# Logging settings
|
||||
LOG_LEVEL = 'INFO'
|
||||
|
||||
# Email relay settings
|
||||
RELAY_TIMEOUT = 10
|
||||
|
||||
# TLS settings
|
||||
TLS_CERT_FILE = 'email_server/ssl_certs/server.crt'
|
||||
TLS_KEY_FILE = 'email_server/ssl_certs/server.key'
|
||||
|
||||
# DKIM settings
|
||||
DKIM_SELECTOR = 'default'
|
||||
DKIM_KEY_SIZE = 2048
|
||||
@@ -2,15 +2,19 @@
|
||||
DKIM key management and email signing functionality.
|
||||
"""
|
||||
|
||||
import logging
|
||||
import dkim
|
||||
from cryptography.hazmat.primitives import serialization, hashes
|
||||
from cryptography.hazmat.primitives.asymmetric import rsa
|
||||
from datetime import datetime
|
||||
from email_server.models import Session, Domain, DKIMKey
|
||||
from email_server.config import DKIM_SELECTOR, DKIM_KEY_SIZE
|
||||
from email_server.settings_loader import load_settings
|
||||
from email_server.tool_box import get_logger
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
settings = load_settings()
|
||||
DKIM_SELECTOR = settings['DKIM']['DKIM_SELECTOR']
|
||||
DKIM_KEY_SIZE = int(settings['DKIM']['DKIM_KEY_SIZE'])
|
||||
|
||||
logger = get_logger()
|
||||
|
||||
class DKIMManager:
|
||||
"""Manages DKIM keys and email signing."""
|
||||
@@ -31,7 +35,7 @@ class DKIMManager:
|
||||
# Check if DKIM key already exists
|
||||
existing_key = session.query(DKIMKey).filter_by(domain_id=domain.id, is_active=True).first()
|
||||
if existing_key:
|
||||
logger.info(f"DKIM key already exists for domain {domain_name}")
|
||||
logger.debug(f"DKIM key already exists for domain {domain_name}")
|
||||
return True
|
||||
|
||||
# Generate RSA key pair
|
||||
@@ -66,7 +70,7 @@ class DKIMManager:
|
||||
session.add(dkim_key)
|
||||
session.commit()
|
||||
|
||||
logger.info(f"Generated DKIM key for domain: {domain_name}")
|
||||
logger.debug(f"Generated DKIM key for domain: {domain_name}")
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
@@ -156,7 +160,7 @@ class DKIMManager:
|
||||
# Combine signature with original content
|
||||
signed_content = signature + email_bytes
|
||||
|
||||
logger.info(f"Successfully signed email for domain: {domain_name}")
|
||||
logger.debug(f"Successfully signed email for domain: {domain_name}")
|
||||
|
||||
# Return as string if input was string
|
||||
if isinstance(email_content, str):
|
||||
@@ -203,7 +207,7 @@ class DKIMManager:
|
||||
).first()
|
||||
|
||||
if not existing_key:
|
||||
logger.info(f"Generating DKIM key for existing domain: {domain.domain_name}")
|
||||
logger.debug(f"Generating DKIM key for existing domain: {domain.domain_name}")
|
||||
self.generate_dkim_keypair(domain.domain_name)
|
||||
|
||||
except Exception as e:
|
||||
|
||||
@@ -5,11 +5,11 @@ Email relay functionality for the SMTP server.
|
||||
import dns.resolver
|
||||
import smtplib
|
||||
import ssl
|
||||
import logging
|
||||
from datetime import datetime
|
||||
from email_server.models import Session, EmailLog
|
||||
from email_server.tool_box import get_logger
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
logger = get_logger()
|
||||
|
||||
class EmailRelay:
|
||||
"""Handles relaying emails to recipient mail servers."""
|
||||
@@ -29,7 +29,7 @@ class EmailRelay:
|
||||
# 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}')
|
||||
logger.debug(f'Found MX record for {domain}: {mx_host}')
|
||||
except Exception as e:
|
||||
logger.error(f'Failed to resolve MX for {domain}: {e}')
|
||||
return False
|
||||
@@ -56,14 +56,14 @@ class EmailRelay:
|
||||
# Check if server supports STARTTLS
|
||||
relay_server.ehlo()
|
||||
if relay_server.has_extn('starttls'):
|
||||
logger.info(f'Starting TLS connection to {mx_host}')
|
||||
logger.debug(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}')
|
||||
logger.debug(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:
|
||||
@@ -71,7 +71,7 @@ class EmailRelay:
|
||||
|
||||
# Send the email
|
||||
relay_server.sendmail(mail_from, rcpt, content)
|
||||
logger.info(f'Successfully relayed email to {rcpt} via {mx_host}')
|
||||
logger.debug(f'Successfully relayed email to {rcpt} via {mx_host}')
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
@@ -86,7 +86,7 @@ class EmailRelay:
|
||||
# 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}')
|
||||
logger.debug(f'Trying backup MX record: {backup_mx}')
|
||||
|
||||
try:
|
||||
with smtplib.SMTP(backup_mx, 25, timeout=self.timeout) as backup_server:
|
||||
@@ -101,12 +101,12 @@ class EmailRelay:
|
||||
context.verify_mode = ssl.CERT_NONE
|
||||
backup_server.starttls(context=context)
|
||||
backup_server.ehlo()
|
||||
logger.info(f'TLS connection established to backup {backup_mx}')
|
||||
logger.debug(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}')
|
||||
logger.debug(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}')
|
||||
|
||||
@@ -6,8 +6,11 @@ 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 email_server.config import DATABASE_URL
|
||||
from email_server.tool_box import ensure_folder_exists
|
||||
from email_server.settings_loader import load_settings
|
||||
from email_server.tool_box import ensure_folder_exists, get_logger
|
||||
|
||||
settings = load_settings()
|
||||
DATABASE_URL = settings['Database']['DATABASE_URL']
|
||||
|
||||
ensure_folder_exists(DATABASE_URL)
|
||||
|
||||
@@ -16,6 +19,8 @@ Base = declarative_base()
|
||||
engine = create_engine(DATABASE_URL, echo=False)
|
||||
Session = sessionmaker(bind=engine)
|
||||
|
||||
logger = get_logger()
|
||||
|
||||
class Domain(Base):
|
||||
__tablename__ = 'domains'
|
||||
id = Column(Integer, primary_key=True)
|
||||
|
||||
@@ -4,10 +4,10 @@ Main server file that ties all modules together.
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
import logging
|
||||
from email_server.settings_loader import load_settings
|
||||
from email_server.tool_box import get_logger
|
||||
|
||||
# Import our modules
|
||||
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
|
||||
@@ -15,12 +15,14 @@ from email_server.dkim_manager import DKIMManager
|
||||
from aiosmtpd.controller import Controller
|
||||
from aiosmtpd.smtp import SMTP as AIOSMTP
|
||||
|
||||
# Configure logging
|
||||
logging.basicConfig(
|
||||
level=getattr(logging, LOG_LEVEL),
|
||||
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
|
||||
)
|
||||
logger = logging.getLogger(__name__)
|
||||
settings = load_settings()
|
||||
SMTP_PORT = int(settings['Server']['SMTP_PORT'])
|
||||
SMTP_TLS_PORT = int(settings['Server']['SMTP_TLS_PORT'])
|
||||
HOSTNAME = settings['Server']['HOSTNAME']
|
||||
LOG_LEVEL = settings['Logging']['LOG_LEVEL']
|
||||
BIND_IP = settings['Server']['BIND_IP']
|
||||
|
||||
logger = get_logger()
|
||||
|
||||
# Enable asyncio debugging
|
||||
try:
|
||||
@@ -32,14 +34,14 @@ except RuntimeError:
|
||||
|
||||
async def start_server():
|
||||
"""Main server function."""
|
||||
logger.info("Starting SMTP Server with DKIM support...")
|
||||
logger.debug("Starting SMTP Server with DKIM support...")
|
||||
|
||||
# Initialize database
|
||||
logger.info("Initializing database...")
|
||||
logger.debug("Initializing database...")
|
||||
create_tables()
|
||||
|
||||
# Initialize DKIM manager and generate keys for domains without them
|
||||
logger.info("Initializing DKIM manager...")
|
||||
logger.debug("Initializing DKIM manager...")
|
||||
dkim_manager = DKIMManager()
|
||||
dkim_manager.initialize_default_keys()
|
||||
|
||||
@@ -53,7 +55,7 @@ async def start_server():
|
||||
domain = Domain(domain_name='example.com', requires_auth=True)
|
||||
session.add(domain)
|
||||
session.commit()
|
||||
logger.info("Added example.com domain")
|
||||
logger.debug("Added example.com domain")
|
||||
|
||||
# Add test user if not exists
|
||||
user = session.query(User).filter_by(email='test@example.com').first()
|
||||
@@ -65,7 +67,7 @@ async def start_server():
|
||||
)
|
||||
session.add(user)
|
||||
session.commit()
|
||||
logger.info("Added test user: test@example.com")
|
||||
logger.debug("Added test user: test@example.com")
|
||||
|
||||
# Add whitelisted IP if not exists
|
||||
whitelist = session.query(WhitelistedIP).filter_by(ip_address='127.0.0.1').first()
|
||||
@@ -73,7 +75,7 @@ async def start_server():
|
||||
whitelist = WhitelistedIP(ip_address='127.0.0.1', domain_id=domain.id)
|
||||
session.add(whitelist)
|
||||
session.commit()
|
||||
logger.info("Added whitelisted IP: 127.0.0.1")
|
||||
logger.debug("Added whitelisted IP: 127.0.0.1")
|
||||
except Exception as e:
|
||||
session.rollback()
|
||||
logger.error(f"Error adding test data: {e}")
|
||||
@@ -81,7 +83,7 @@ async def start_server():
|
||||
session.close()
|
||||
|
||||
# Generate SSL certificate if it doesn't exist
|
||||
logger.info("Checking SSL certificates...")
|
||||
logger.debug("Checking SSL certificates...")
|
||||
if not generate_self_signed_cert():
|
||||
logger.error("Failed to generate SSL certificate")
|
||||
return
|
||||
@@ -101,7 +103,7 @@ async def start_server():
|
||||
port=SMTP_PORT
|
||||
)
|
||||
controller_plain.start()
|
||||
logger.info(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
|
||||
handler_tls = CustomSMTPHandler()
|
||||
@@ -126,15 +128,15 @@ async def start_server():
|
||||
port=SMTP_TLS_PORT
|
||||
)
|
||||
controller_tls.start()
|
||||
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.debug(f' - Plain SMTP (IP whitelist): {BIND_IP}:{SMTP_PORT}')
|
||||
logger.debug(f' - STARTTLS SMTP (auth required): {BIND_IP}:{SMTP_TLS_PORT}')
|
||||
logger.debug('Management commands:')
|
||||
logger.debug(' python cli_tools.py --help')
|
||||
|
||||
try:
|
||||
await asyncio.Event().wait()
|
||||
except KeyboardInterrupt:
|
||||
logger.info('Shutting down SMTP servers...')
|
||||
logger.debug('Shutting down SMTP servers...')
|
||||
controller_plain.stop()
|
||||
controller_tls.stop()
|
||||
logger.info('SMTP servers stopped.')
|
||||
logger.debug('SMTP servers stopped.')
|
||||
54
email_server/settings_loader.py
Normal file
54
email_server/settings_loader.py
Normal file
@@ -0,0 +1,54 @@
|
||||
"""
|
||||
Settings loader for the SMTP server.
|
||||
Automatically generates settings.ini with default values if not present.
|
||||
"""
|
||||
|
||||
import configparser
|
||||
from pathlib import Path
|
||||
|
||||
SETTINGS_PATH = Path(__file__).parent.parent / 'settings.ini'
|
||||
|
||||
# Default values for settings.ini
|
||||
DEFAULTS = {
|
||||
'Server': {
|
||||
'SMTP_PORT': '4025',
|
||||
'SMTP_TLS_PORT': '40587',
|
||||
'HOSTNAME': 'localhost',
|
||||
'BIND_IP': '0.0.0.0',
|
||||
},
|
||||
'Database': {
|
||||
'DATABASE_URL': 'sqlite:///email_server/server_data/smtp_server.db',
|
||||
},
|
||||
'Logging': {
|
||||
'LOG_LEVEL': 'INFO',
|
||||
'hide_info_aiosmtpd': 'true',
|
||||
},
|
||||
'Relay': {
|
||||
'RELAY_TIMEOUT': '10',
|
||||
},
|
||||
'TLS': {
|
||||
'TLS_CERT_FILE': 'email_server/ssl_certs/server.crt',
|
||||
'TLS_KEY_FILE': 'email_server/ssl_certs/server.key',
|
||||
},
|
||||
'DKIM': {
|
||||
'DKIM_SELECTOR': 'default',
|
||||
'DKIM_KEY_SIZE': '2048',
|
||||
},
|
||||
}
|
||||
|
||||
def generate_settings_ini(settings_path: Path = SETTINGS_PATH) -> None:
|
||||
"""Generate settings.ini with default values if it does not exist."""
|
||||
if settings_path.exists():
|
||||
return
|
||||
config_parser = configparser.ConfigParser()
|
||||
for section, values in DEFAULTS.items():
|
||||
config_parser[section] = values
|
||||
with open(settings_path, 'w') as f:
|
||||
config_parser.write(f)
|
||||
|
||||
def load_settings(settings_path: Path = SETTINGS_PATH) -> configparser.ConfigParser:
|
||||
"""Load settings from settings.ini, generating it if needed."""
|
||||
generate_settings_ini(settings_path)
|
||||
config_parser = configparser.ConfigParser()
|
||||
config_parser.read(settings_path)
|
||||
return config_parser
|
||||
@@ -2,7 +2,6 @@
|
||||
SMTP handler for processing incoming emails.
|
||||
"""
|
||||
|
||||
import logging
|
||||
import uuid
|
||||
from datetime import datetime
|
||||
from aiosmtpd.smtp import SMTP as AIOSMTP, AuthResult
|
||||
@@ -10,8 +9,9 @@ from aiosmtpd.controller import Controller
|
||||
from email_server.auth import Authenticator, IPAuthenticator
|
||||
from email_server.email_relay import EmailRelay
|
||||
from email_server.dkim_manager import DKIMManager
|
||||
from email_server.tool_box import get_logger
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
logger = get_logger()
|
||||
|
||||
class CombinedAuthenticator:
|
||||
"""Combined authenticator that tries username/password first, then falls back to IP whitelist."""
|
||||
@@ -50,7 +50,7 @@ class CustomSMTPHandler:
|
||||
"""Handle incoming email data."""
|
||||
try:
|
||||
message_id = str(uuid.uuid4())
|
||||
logger.info(f'Received email {message_id} from {envelope.mail_from} to {envelope.rcpt_tos}')
|
||||
logger.debug(f'Received email {message_id} from {envelope.mail_from} to {envelope.rcpt_tos}')
|
||||
|
||||
# Convert content to string if it's bytes
|
||||
if isinstance(envelope.content, bytes):
|
||||
@@ -69,7 +69,7 @@ class CustomSMTPHandler:
|
||||
# Check if signing was successful (content changed)
|
||||
dkim_signed = signed_content != content
|
||||
if dkim_signed:
|
||||
logger.info(f'Email {message_id} signed with DKIM for domain {sender_domain}')
|
||||
logger.debug(f'Email {message_id} signed with DKIM for domain {sender_domain}')
|
||||
|
||||
# Relay the email
|
||||
success = self.email_relay.relay_email(
|
||||
@@ -91,7 +91,7 @@ class CustomSMTPHandler:
|
||||
)
|
||||
|
||||
if success:
|
||||
logger.info(f'Email {message_id} successfully relayed')
|
||||
logger.debug(f'Email {message_id} successfully relayed')
|
||||
return '250 Message accepted for delivery'
|
||||
else:
|
||||
logger.error(f'Email {message_id} failed to relay')
|
||||
|
||||
@@ -4,12 +4,15 @@ TLS utilities for the SMTP server.
|
||||
|
||||
import ssl
|
||||
import os
|
||||
import logging
|
||||
from OpenSSL import crypto
|
||||
from email_server.config import TLS_CERT_FILE, TLS_KEY_FILE
|
||||
from email_server.tool_box import ensure_folder_exists
|
||||
from email_server.settings_loader import load_settings
|
||||
from email_server.tool_box import ensure_folder_exists, get_logger
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
settings = load_settings()
|
||||
TLS_CERT_FILE = settings['TLS']['TLS_CERT_FILE']
|
||||
TLS_KEY_FILE = settings['TLS']['TLS_KEY_FILE']
|
||||
|
||||
logger = get_logger()
|
||||
|
||||
ensure_folder_exists(TLS_CERT_FILE)
|
||||
ensure_folder_exists(TLS_KEY_FILE)
|
||||
@@ -17,11 +20,11 @@ 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):
|
||||
logger.info("SSL certificate already exists")
|
||||
logger.debug("SSL certificate already exists")
|
||||
return True
|
||||
|
||||
try:
|
||||
logger.info("Generating self-signed SSL certificate...")
|
||||
logger.debug("Generating self-signed SSL certificate...")
|
||||
|
||||
# Generate private key
|
||||
k = crypto.PKey()
|
||||
@@ -47,7 +50,7 @@ def generate_self_signed_cert():
|
||||
with open(TLS_KEY_FILE, 'wb') as f:
|
||||
f.write(crypto.dump_privatekey(crypto.FILETYPE_PEM, k))
|
||||
|
||||
logger.info(f"SSL certificate generated: {TLS_CERT_FILE}, {TLS_KEY_FILE}")
|
||||
logger.debug(f"SSL certificate generated: {TLS_CERT_FILE}, {TLS_KEY_FILE}")
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
@@ -60,7 +63,7 @@ def create_ssl_context():
|
||||
ssl_context = ssl.create_default_context(ssl.Purpose.CLIENT_AUTH)
|
||||
ssl_context.load_cert_chain(certfile=TLS_CERT_FILE, keyfile=TLS_KEY_FILE)
|
||||
ssl_context.set_ciphers('DEFAULT') # Relax ciphers for compatibility
|
||||
logger.info('SSL context created successfully')
|
||||
logger.debug('SSL context created successfully')
|
||||
return ssl_context
|
||||
except Exception as e:
|
||||
logger.error(f'Failed to create SSL context: {e}')
|
||||
|
||||
@@ -3,6 +3,10 @@ Utility functions for the email server.
|
||||
"""
|
||||
|
||||
import os
|
||||
import logging
|
||||
from email_server.settings_loader import load_settings
|
||||
|
||||
settings = load_settings()
|
||||
|
||||
def ensure_folder_exists(filepath):
|
||||
"""
|
||||
@@ -10,4 +14,46 @@ def ensure_folder_exists(filepath):
|
||||
"""
|
||||
if filepath.startswith("sqlite:///"):
|
||||
filepath = filepath.replace("sqlite:///", "", 1)
|
||||
os.makedirs(os.path.dirname(filepath), exist_ok=True)
|
||||
os.makedirs(os.path.dirname(filepath), exist_ok=True)
|
||||
|
||||
|
||||
|
||||
def setup_logging():
|
||||
"""
|
||||
Set up global logging configuration using settings.ini.
|
||||
Should be called once at program entry point.
|
||||
Optionally hides aiosmtpd 'mail.log' INFO logs when global logging is INFO based on settings.
|
||||
"""
|
||||
log_level = getattr(logging, settings['Logging']['LOG_LEVEL'], logging.INFO)
|
||||
log_format = '%(asctime)s - %(name)s - %(levelname)s - %(message)s'
|
||||
if not logging.getLogger().hasHandlers():
|
||||
logging.basicConfig(level=log_level, format=log_format)
|
||||
else:
|
||||
logging.getLogger().setLevel(log_level)
|
||||
|
||||
# Hide aiosmtpd INFO logs if configured `hide_info_aiosmtpd = true`
|
||||
hide_info_aiosmtpd = settings['Logging'].get('hide_info_aiosmtpd', 'true').lower() == 'true'
|
||||
if hide_info_aiosmtpd and log_level == logging.INFO:
|
||||
# Set aiosmtpd mail.log to WARNING level to hide INFO logs
|
||||
logging.getLogger('mail.log').setLevel(logging.WARNING)
|
||||
|
||||
def get_logger(name=None):
|
||||
"""
|
||||
Get a logger with the given name (default: module name).
|
||||
Ensures logging is set up before returning the logger.
|
||||
"""
|
||||
setup_logging()
|
||||
if name is None:
|
||||
# Get the caller's file name
|
||||
import inspect
|
||||
frame = inspect.currentframe()
|
||||
# Go back one frame to the caller
|
||||
caller_frame = frame.f_back
|
||||
filename = caller_frame.f_globals.get('__file__', None)
|
||||
if filename:
|
||||
base = os.path.basename(filename)
|
||||
name, ext = os.path.splitext(base)
|
||||
name = name if ext == '.py' else base
|
||||
else:
|
||||
name = '__main__'
|
||||
return logging.getLogger(name)
|
||||
@@ -1,13 +1,13 @@
|
||||
from email_server.server_runner import start_server
|
||||
from email_server.tool_box import get_logger
|
||||
import asyncio
|
||||
import sys
|
||||
import logging
|
||||
|
||||
logging.basicConfig(level=logging.INFO)
|
||||
logger = logging.getLogger(__name__)
|
||||
logger = get_logger()
|
||||
|
||||
if __name__ == '__main__':
|
||||
try:
|
||||
logger.info('Server started')
|
||||
asyncio.run(start_server())
|
||||
except KeyboardInterrupt:
|
||||
logger.info('Server interrupted by user')
|
||||
|
||||
BIN
tests/Hello.jpg
Normal file
BIN
tests/Hello.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 76 KiB |
68
tests/bash_send_email.sh
Normal file
68
tests/bash_send_email.sh
Normal file
@@ -0,0 +1,68 @@
|
||||
#!/bin/bash
|
||||
sender="test@example.com"
|
||||
receiver="info@example.com"
|
||||
password="testpass123"
|
||||
domain="example.com"
|
||||
body_content_file="@tests/email_body.txt"
|
||||
SMTP_PORT=4025
|
||||
SMTP_TLS_PORT=40587
|
||||
cc_recipient="targetcc@example.com"
|
||||
bcc_recipient="targetbcc@example.com"
|
||||
|
||||
<<com
|
||||
python -m email_server.cli_tools add-domain $domain
|
||||
python -m email_server.cli_tools add-user $sender $password $domain
|
||||
python -m email_server.cli_tools add-ip 127.0.0.1 $domain
|
||||
python -m email_server.cli_tools add-ip 10.100.111.1 $domain
|
||||
python -m email_server.cli_tools generate-dkim $domain
|
||||
|
||||
# options to add CC and BCC recipients for swaks
|
||||
--cc $cc_recipient
|
||||
--bcc $bcc_recipient
|
||||
com
|
||||
|
||||
swaks --to $receiver \
|
||||
--from $sender \
|
||||
--server localhost \
|
||||
--port $SMTP_TLS_PORT \
|
||||
--auth LOGIN \
|
||||
--auth-user $sender \
|
||||
--auth-password $password \
|
||||
--tls \
|
||||
--header "Subject: TLS - Large body email" \
|
||||
--body $body_content_file \
|
||||
--attach tests/email_body.txt \
|
||||
--attach tests/Hello.jpg
|
||||
|
||||
swaks --to $receiver \
|
||||
--from $sender \
|
||||
--server localhost \
|
||||
--port $SMTP_PORT \
|
||||
--auth LOGIN \
|
||||
--auth-user $sender \
|
||||
--auth-password $password \
|
||||
--data "Subject: Test Email - authenticated\n\nThis is the message body."
|
||||
|
||||
swaks --to $receiver \
|
||||
--from $sender \
|
||||
--server localhost \
|
||||
--port $SMTP_TLS_PORT \
|
||||
--auth LOGIN \
|
||||
--auth-user $sender \
|
||||
--auth-password $password \
|
||||
--tls \
|
||||
--data "Subject: Test via STARTTLS - authenticated\n\nThis is the body."
|
||||
|
||||
swaks --to $receiver \
|
||||
--from $sender \
|
||||
--server localhost \
|
||||
--port $SMTP_TLS_PORT \
|
||||
--tls \
|
||||
--data "Subject: Test via STARTTLS - no auth\n\nThis is the body."
|
||||
com
|
||||
|
||||
swaks --to $receiver \
|
||||
--from $sender \
|
||||
--server localhost \
|
||||
--port $SMTP_PORT \
|
||||
--data "Subject: Test Email - no auth\n\nThis is the message body."
|
||||
Reference in New Issue
Block a user