Merge pull request #2 from ghostersk/testing

Fixed email headers order and DKIM signing
This commit is contained in:
ghostersk
2025-06-07 09:00:27 +01:00
committed by GitHub
7 changed files with 312 additions and 87 deletions
+1 -1
View File
@@ -233,7 +233,7 @@ def main():
print("Database tables created successfully") print("Database tables created successfully")
elif args.command == 'add-domain': elif args.command == 'add-domain':
add_domain(args.domain, not args.no_auth) add_domain(args.domain)
elif args.command == 'add-user': elif args.command == 'add-user':
add_user(args.email, args.password, args.domain) add_user(args.email, args.password, args.domain)
+15 -5
View File
@@ -7,6 +7,7 @@ import smtplib
import ssl import ssl
from datetime import datetime from datetime import datetime
from email_server.models import Session, EmailLog from email_server.models import Session, EmailLog
from email_server.settings_loader import load_settings
from email_server.tool_box import get_logger from email_server.tool_box import get_logger
logger = get_logger() logger = get_logger()
@@ -16,6 +17,11 @@ class EmailRelay:
def __init__(self): def __init__(self):
self.timeout = 30 # Increased timeout for TLS negotiations self.timeout = 30 # Increased timeout for TLS negotiations
# Get the configured hostname for HELO/EHLO identification
settings = load_settings()
self.hostname = settings['Server'].get('helo_hostname',
settings['Server'].get('hostname', 'localhost'))
logger.debug(f"EmailRelay initialized with hostname: {self.hostname}")
def relay_email(self, mail_from, rcpt_tos, content): def relay_email(self, mail_from, rcpt_tos, content):
"""Relay email to recipient's mail server with opportunistic TLS.""" """Relay email to recipient's mail server with opportunistic TLS."""
@@ -53,8 +59,9 @@ class EmailRelay:
# Try to enable TLS if the server supports it # Try to enable TLS if the server supports it
try: try:
# Check if server supports STARTTLS # Check if server supports STARTTLS - use proper hostname for EHLO
relay_server.ehlo() logger.debug(f'Sending EHLO {self.hostname} to {mx_host}')
relay_server.ehlo(self.hostname)
if relay_server.has_extn('starttls'): if relay_server.has_extn('starttls'):
logger.debug(f'Starting TLS connection to {mx_host}') logger.debug(f'Starting TLS connection to {mx_host}')
context = ssl.create_default_context() context = ssl.create_default_context()
@@ -62,7 +69,8 @@ class EmailRelay:
context.check_hostname = False context.check_hostname = False
context.verify_mode = ssl.CERT_NONE context.verify_mode = ssl.CERT_NONE
relay_server.starttls(context=context) relay_server.starttls(context=context)
relay_server.ehlo() # Say hello again after STARTTLS logger.debug(f'Sending EHLO {self.hostname} again after STARTTLS to {mx_host}')
relay_server.ehlo(self.hostname) # Say hello again after STARTTLS with proper hostname
logger.debug(f'TLS connection established to {mx_host}') logger.debug(f'TLS connection established to {mx_host}')
else: else:
logger.warning(f'Server {mx_host} does not support STARTTLS, using plain text') logger.warning(f'Server {mx_host} does not support STARTTLS, using plain text')
@@ -94,13 +102,15 @@ class EmailRelay:
# Try TLS with backup server too # Try TLS with backup server too
try: try:
backup_server.ehlo() logger.debug(f'Sending EHLO {self.hostname} to backup {backup_mx}')
backup_server.ehlo(self.hostname)
if backup_server.has_extn('starttls'): if backup_server.has_extn('starttls'):
context = ssl.create_default_context() context = ssl.create_default_context()
context.check_hostname = False context.check_hostname = False
context.verify_mode = ssl.CERT_NONE context.verify_mode = ssl.CERT_NONE
backup_server.starttls(context=context) backup_server.starttls(context=context)
backup_server.ehlo() logger.debug(f'Sending EHLO {self.hostname} again after STARTTLS to backup {backup_mx}')
backup_server.ehlo(self.hostname)
logger.debug(f'TLS connection established to backup {backup_mx}') logger.debug(f'TLS connection established to backup {backup_mx}')
except Exception: except Exception:
logger.warning(f'STARTTLS failed with backup {backup_mx}, using plain text') logger.warning(f'STARTTLS failed with backup {backup_mx}, using plain text')
+9 -21
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 EnhancedCustomSMTPHandler, PlainController from email_server.smtp_handler import EnhancedCustomSMTPHandler, PlainController, TLSController
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
@@ -18,7 +18,7 @@ from aiosmtpd.smtp import SMTP as AIOSMTP
settings = load_settings() settings = load_settings()
SMTP_PORT = int(settings['Server']['SMTP_PORT']) SMTP_PORT = int(settings['Server']['SMTP_PORT'])
SMTP_TLS_PORT = int(settings['Server']['SMTP_TLS_PORT']) SMTP_TLS_PORT = int(settings['Server']['SMTP_TLS_PORT'])
HOSTNAME = settings['Server']['HOSTNAME'] HOSTNAME = settings['Server'].get('helo_hostname', settings['Server'].get('hostname', 'localhost'))
LOG_LEVEL = settings['Logging']['LOG_LEVEL'] LOG_LEVEL = settings['Logging']['LOG_LEVEL']
BIND_IP = settings['Server']['BIND_IP'] BIND_IP = settings['Server']['BIND_IP']
@@ -94,37 +94,25 @@ async def start_server():
logger.error("Failed to create SSL context") logger.error("Failed to create SSL context")
return return
logger.debug(f"SSL context created: {ssl_context}")
logger.debug(f"SSL context type: {type(ssl_context)}")
# Start plain SMTP server (with IP whitelist fallback) # Start plain SMTP server (with IP whitelist fallback)
handler_plain = EnhancedCustomSMTPHandler() handler_plain = EnhancedCustomSMTPHandler()
controller_plain = PlainController( controller_plain = PlainController(
handler_plain, handler_plain,
hostname=BIND_IP, hostname=HOSTNAME, # Use proper hostname for HELO identification
server_hostname="TestEnvironment",
port=SMTP_PORT port=SMTP_PORT
) )
controller_plain.start() controller_plain.start()
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 the updated TLSController
handler_tls = EnhancedCustomSMTPHandler() handler_tls = EnhancedCustomSMTPHandler()
# Define TLS controller class with ssl_context in closure (like original)
class TLSController(Controller):
def factory(self):
return AIOSMTP(
self.handler,
tls_context=ssl_context, # Use ssl_context from closure
require_starttls=False, # 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,
hostname=self.hostname
)
controller_tls = TLSController( controller_tls = TLSController(
handler_tls, handler_tls,
hostname=BIND_IP, ssl_context=ssl_context,
server_hostname="TestEnvironment", hostname=HOSTNAME, # Use proper hostname for HELO identification
port=SMTP_TLS_PORT port=SMTP_TLS_PORT
) )
controller_tls.start() controller_tls.start()
+29 -5
View File
@@ -8,40 +8,64 @@ from pathlib import Path
SETTINGS_PATH = Path(__file__).parent.parent / 'settings.ini' SETTINGS_PATH = Path(__file__).parent.parent / 'settings.ini'
# Default values for settings.ini # Default values and comments for settings.ini
DEFAULTS = { DEFAULTS = {
'Server': { 'Server': {
'; Server configuration for SMTP ports and hostname': None,
'; Plain SMTP port for internal/whitelisted IPs': None,
'SMTP_PORT': '4025', 'SMTP_PORT': '4025',
'; STARTTLS SMTP port for authenticated users': None,
'SMTP_TLS_PORT': '40587', 'SMTP_TLS_PORT': '40587',
'HOSTNAME': 'localhost', '; Server hostname for HELO/EHLO identification': None,
'HOSTNAME': 'mail.example.com',
'; Override HELO hostname': None,
'helo_hostname': 'mail.example.com',
'; IP address to bind to (0.0.0.0 = all interfaces), on Windows must use specific IP': None,
'BIND_IP': '0.0.0.0', 'BIND_IP': '0.0.0.0',
'; Custom server banner (to make it empty use "" must be double quotes)': None,
'server_banner': "",
}, },
'Database': { 'Database': {
'; Database configuration': None,
'DATABASE_URL': 'sqlite:///email_server/server_data/smtp_server.db', 'DATABASE_URL': 'sqlite:///email_server/server_data/smtp_server.db',
}, },
'Logging': { 'Logging': {
'; Logging configuration': None,
'; Log level: DEBUG, INFO, WARNING, ERROR, CRITICAL': None,
'LOG_LEVEL': 'INFO', 'LOG_LEVEL': 'INFO',
'; Hide verbose aiosmtpd INFO messages when LOG_LEVEL = INFO': None,
'hide_info_aiosmtpd': 'true', 'hide_info_aiosmtpd': 'true',
}, },
'Relay': { 'Relay': {
'; Timeout in seconds for external SMTP connections': None,
'RELAY_TIMEOUT': '10', 'RELAY_TIMEOUT': '10',
}, },
'TLS': { 'TLS': {
'; TLS/SSL certificate configuration': None,
'TLS_CERT_FILE': 'email_server/ssl_certs/server.crt', 'TLS_CERT_FILE': 'email_server/ssl_certs/server.crt',
'TLS_KEY_FILE': 'email_server/ssl_certs/server.key', 'TLS_KEY_FILE': 'email_server/ssl_certs/server.key',
}, },
'DKIM': { 'DKIM': {
'; DKIM signing configuration': None,
'; RSA key size for DKIM keys (1024, 2048, 4096)': None,
'DKIM_KEY_SIZE': '2048', 'DKIM_KEY_SIZE': '2048',
}, },
} }
def generate_settings_ini(settings_path: Path = SETTINGS_PATH) -> None: def generate_settings_ini(settings_path: Path = SETTINGS_PATH) -> None:
"""Generate settings.ini with default values if it does not exist.""" """Generate settings.ini with default values and comments if it does not exist."""
if settings_path.exists(): if settings_path.exists():
return return
config_parser = configparser.ConfigParser() config_parser = configparser.ConfigParser(allow_no_value=True)
for section, values in DEFAULTS.items(): for section, values in DEFAULTS.items():
config_parser[section] = values config_parser.add_section(section)
for key, value in values.items():
if key.startswith(';'):
# This is a comment line
config_parser.set(section, key)
else:
# This is a setting with value
config_parser.set(section, key, value)
with open(settings_path, 'w') as f: with open(settings_path, 'w') as f:
config_parser.write(f) config_parser.write(f)
+216 -49
View File
@@ -9,17 +9,32 @@ Security Features:
""" """
import uuid import uuid
import email.utils
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 EnhancedAuthenticator, EnhancedIPAuthenticator, validate_sender_authorization, get_authenticated_domain_id 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.settings_loader import load_settings
from email_server.tool_box import get_logger from email_server.tool_box import get_logger
logger = get_logger() logger = get_logger()
class CustomSMTP(AIOSMTP):
"""Custom SMTP class with configurable banner."""
def __init__(self, *args, **kwargs):
# Sets Custom SMTP banner from settings
settings = load_settings()
_banner_message = settings['Server'].get('server_banner', '')
if _banner_message == '""':
_banner_message = ''
self.custom_banner = _banner_message
super().__init__(*args, **kwargs)
# Override the __ident__ to use our custom banner
self.__ident__ = self.custom_banner
class EnhancedCombinedAuthenticator: class EnhancedCombinedAuthenticator:
""" """
Enhanced combined authenticator with sender validation support. Enhanced combined authenticator with sender validation support.
@@ -61,51 +76,182 @@ class EnhancedCustomSMTPHandler:
self.auth_require_tls = False self.auth_require_tls = False
self.auth_methods = ['LOGIN', 'PLAIN'] self.auth_methods = ['LOGIN', 'PLAIN']
def _ensure_required_headers(self, content: str, envelope, message_id: str) -> str: def _ensure_required_headers(self, content: str, envelope, message_id: str, custom_headers: list = None) -> str:
"""Ensure all required email headers are present and properly formatted. """Ensure all required email headers are present and properly formatted.
Following RFC 5322 header order and best practices for spam score reduction.
Optimized based on Gmail's header structure for better deliverability.
Args: Args:
content (str): Email content. content (str): Email content.
envelope: SMTP envelope. envelope: SMTP envelope.
message_id (str): Generated message ID. message_id (str): Generated message ID.
custom_headers (list): List of (name, value) tuples for custom headers.
Returns: Returns:
str: Email content with all required headers. str: Email content with all required headers properly formatted.
""" """
import email import email.utils
from email.parser import Parser from email_server.settings_loader import load_settings
from email.policy import default
# Parse the message using the email library try:
msg = Parser(policy=default).parsestr(content) settings = load_settings()
fallback_hostname = settings.get('Server', 'HOSTNAME', fallback='localhost')
server_hostname = settings.get('Server', 'helo_hostname', fallback=fallback_hostname)
# Set or add required headers if missing logger.debug(f"Processing headers for message {message_id}")
if not msg.get('Message-ID'):
msg['Message-ID'] = f"<{message_id}@{envelope.mail_from.split('@')[1] if '@' in envelope.mail_from else 'localhost'}>"
if not msg.get('Date'):
msg['Date'] = email.utils.formatdate(localtime=True)
if not msg.get('From'):
msg['From'] = envelope.mail_from
if not msg.get('To'):
msg['To'] = ', '.join(envelope.rcpt_tos)
if not msg.get('MIME-Version'):
msg['MIME-Version'] = '1.0'
if not msg.get('Content-Type'):
msg['Content-Type'] = 'text/plain; charset=utf-8'
if not msg.get('Subject'):
msg['Subject'] = '(No Subject)'
if not msg.get('Content-Transfer-Encoding'):
msg['Content-Transfer-Encoding'] = '7bit'
# Ensure exactly one blank line between headers and body # Parse the message properly
# The email library will handle this when flattening if isinstance(content, bytes):
from io import StringIO content = content.decode('utf-8', errors='replace')
out = StringIO()
out.write(msg.as_string()) # Split content into lines and normalize line endings
return out.getvalue() lines = content.replace('\r\n', '\n').replace('\r', '\n').split('\n')
# Find header/body boundary and collect existing headers
body_start = 0
existing_headers = {}
original_header_order = []
for i, line in enumerate(lines):
if line.strip() == '':
body_start = i + 1
break
if ':' in line and not line.startswith((' ', '\t')):
header_name, header_value = line.split(':', 1)
header_name_lower = header_name.strip().lower()
header_value = header_value.strip()
# Handle continuation lines
j = i + 1
while j < len(lines) and lines[j].startswith((' ', '\t')):
header_value += ' ' + lines[j].strip()
j += 1
existing_headers[header_name_lower] = header_value
original_header_order.append((header_name.strip(), header_value))
logger.debug(f"Found existing header: {header_name_lower} = {header_value}")
# Extract body and clean it
body_lines = lines[body_start:] if body_start < len(lines) else []
while body_lines and body_lines[-1].strip() == '':
body_lines.pop()
body = '\n'.join(body_lines)
# Build headers in optimized order based on Gmail's structure
required_headers = []
# 1. Message-ID (critical for spam filters)
if 'message-id' in existing_headers:
required_headers.append(f"Message-ID: {existing_headers['message-id']}")
else:
domain = envelope.mail_from.split('@')[1] if '@' in envelope.mail_from else server_hostname.replace('mail.', '')
required_headers.append(f"Message-ID: <{message_id}@{domain}>")
# 2. Date (critical for spam filters)
if 'date' in existing_headers:
required_headers.append(f"Date: {existing_headers['date']}")
else:
date_str = email.utils.formatdate(localtime=True)
required_headers.append(f"Date: {date_str}")
# 3. MIME-Version (declare MIME compliance early)
if 'mime-version' in existing_headers:
required_headers.append(f"MIME-Version: {existing_headers['mime-version']}")
else:
required_headers.append("MIME-Version: 1.0")
# 4. User-Agent (if present, helps with reputation)
if 'user-agent' in existing_headers:
required_headers.append(f"User-Agent: {existing_headers['user-agent']}")
# 5. Content-Language (if present)
if 'content-language' in existing_headers:
required_headers.append(f"Content-Language: {existing_headers['content-language']}")
# 6. To (primary recipients - critical)
if 'to' in existing_headers:
required_headers.append(f"To: {existing_headers['to']}")
else:
to_list = ', '.join(envelope.rcpt_tos)
required_headers.append(f"To: {to_list}")
# 7. From (sender identification - critical)
if 'from' in existing_headers:
required_headers.append(f"From: {existing_headers['from']}")
else:
required_headers.append(f"From: {envelope.mail_from}")
# 8. Subject (message topic - critical)
if 'subject' in existing_headers:
required_headers.append(f"Subject: {existing_headers['subject']}")
else:
required_headers.append("Subject: ")
# 9. Content-Type (media type information)
if 'content-type' in existing_headers:
required_headers.append(f"Content-Type: {existing_headers['content-type']}")
else:
required_headers.append("Content-Type: text/plain; charset=UTF-8; format=flowed")
# 10. Content-Transfer-Encoding
if 'content-transfer-encoding' in existing_headers:
required_headers.append(f"Content-Transfer-Encoding: {existing_headers['content-transfer-encoding']}")
else:
required_headers.append("Content-Transfer-Encoding: 7bit")
# Add custom headers after essential headers but before misc headers
if custom_headers:
for header_name, header_value in custom_headers:
# Skip if already added in essential headers
if header_name.lower() not in ['message-id', 'date', 'mime-version', 'user-agent',
'content-language', 'to', 'from', 'subject',
'content-type', 'content-transfer-encoding']:
required_headers.append(f"{header_name}: {header_value}")
logger.debug(f"Added custom header: {header_name}: {header_value}")
# Add any other existing headers that weren't handled above
essential_headers = {
'message-id', 'date', 'from', 'to', 'subject',
'mime-version', 'content-type', 'content-transfer-encoding',
'user-agent', 'content-language'
}
# Preserve original header names and values for non-essential headers
for header_name, header_value in original_header_order:
if header_name.lower() not in essential_headers:
# Skip custom headers we already added
skip = False
if custom_headers:
for custom_name, _ in custom_headers:
if header_name.lower() == custom_name.lower():
skip = True
break
if not skip:
required_headers.append(f"{header_name}: {header_value}")
# Build final message
final_content = '\r\n'.join(required_headers)
if body.strip():
final_content += '\r\n\r\n' + body
else:
final_content += '\r\n\r\n'
logger.debug(f"Final headers for message {message_id}:")
for header in required_headers:
logger.debug(f" {header}")
return final_content
except Exception as e:
logger.error(f"Error ensuring headers: {e}")
import traceback
logger.error(f"Traceback: {traceback.format_exc()}")
# Fallback to original content if parsing fails
return content
async def handle_DATA(self, server, session, envelope): async def handle_DATA(self, server, session, envelope):
"""Handle incoming email data.""" """Handle incoming email data with improved header management."""
try: try:
message_id = str(uuid.uuid4()) message_id = str(uuid.uuid4())
logger.debug(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}')
@@ -119,26 +265,36 @@ class EnhancedCustomSMTPHandler:
# Extract domain from sender for DKIM signing # Extract domain from sender for DKIM signing
sender_domain = envelope.mail_from.split('@')[1] if '@' in envelope.mail_from else None sender_domain = envelope.mail_from.split('@')[1] if '@' in envelope.mail_from else None
# Ensure required headers are present # Get custom headers before processing
content = self._ensure_required_headers(content, envelope, message_id) custom_headers = []
# Add custom headers before DKIM signing
if sender_domain: if sender_domain:
custom_headers = self.dkim_manager.get_active_custom_headers(sender_domain) custom_headers = self.dkim_manager.get_active_custom_headers(sender_domain)
for header_name, header_value in custom_headers:
# Insert header at the top of the message
content = f"{header_name}: {header_value}\r\n" + content
# Relay the email (all modifications done) # Add beneficial headers for spam score improvement
client_ip = getattr(session, 'peer', ['unknown'])[0] if hasattr(session, 'peer') else None
if client_ip:
# Add X-Originating-IP header (helps with reputation)
custom_headers.append(('X-Originating-IP', f'[{client_ip}]'))
# Add X-Mailer header for identification
custom_headers.append(('X-Mailer', 'NetBro Mail Server 1.0'))
# Add X-Priority header (normal priority)
custom_headers.append(('X-Priority', '3'))
# Ensure required headers are present (including custom headers)
content = self._ensure_required_headers(content, envelope, message_id, custom_headers)
# DKIM-sign the final version of the message (only once, after all modifications)
signed_content = content signed_content = content
dkim_signed = False dkim_signed = False
if sender_domain: if sender_domain:
# DKIM-sign the final version of the message
signed_content = self.dkim_manager.sign_email(content, sender_domain) signed_content = self.dkim_manager.sign_email(content, sender_domain)
dkim_signed = signed_content != content dkim_signed = signed_content != content
if dkim_signed: if dkim_signed:
logger.debug(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 (no further modifications allowed)
success = self.email_relay.relay_email( success = self.email_relay.relay_email(
envelope.mail_from, envelope.mail_from,
envelope.rcpt_tos, envelope.rcpt_tos,
@@ -200,28 +356,39 @@ class TLSController(Controller):
"""Custom controller with TLS support - modeled after the working original.""" """Custom controller with TLS support - modeled after the working original."""
def __init__(self, handler, ssl_context, hostname='localhost', port=40587): def __init__(self, handler, ssl_context, hostname='localhost', port=40587):
self.ssl_context = ssl_context logger.debug(f"TLSController __init__: ssl_context={ssl_context is not None}")
super().__init__(handler, hostname=hostname, port=port) self._ssl_context = ssl_context # Use private attribute to avoid conflicts
self.smtp_hostname = hostname # Store for HELO identification
super().__init__(handler, hostname='0.0.0.0', port=port) # Bind to all interfaces
def factory(self): def factory(self):
return AIOSMTP( logger.debug(f"TLSController factory: ssl_context={self._ssl_context is not None}")
logger.debug(f"TLSController factory: ssl_context object={self._ssl_context}")
logger.debug(f"TLSController factory: hostname={self.smtp_hostname}")
smtp_instance = CustomSMTP(
self.handler, self.handler,
tls_context=self.ssl_context, tls_context=self._ssl_context,
require_starttls=True, # Don't force STARTTLS, but make it available require_starttls=False, # Don't require STARTTLS immediately, but make it available
auth_require_tls=True, # If auth is used, require TLS auth_require_tls=True, # If auth is used, require TLS
authenticator=self.handler.combined_authenticator, authenticator=self.handler.combined_authenticator,
decode_data=True, decode_data=True,
hostname=self.hostname hostname=self.smtp_hostname # Use proper hostname for HELO
) )
logger.debug(f"TLSController CustomSMTP instance created with TLS: {hasattr(smtp_instance, 'tls_context')}")
return smtp_instance
class PlainController(Controller): class PlainController(Controller):
"""Controller for plain SMTP with username/password and IP-based authentication.""" """Controller for plain SMTP with username/password and IP-based authentication."""
def __init__(self, handler, hostname='localhost', port=4025):
self.smtp_hostname = hostname # Store for HELO identification
super().__init__(handler, hostname='0.0.0.0', port=port) # Bind to all interfaces
def factory(self): def factory(self):
return AIOSMTP( return CustomSMTP(
self.handler, self.handler,
authenticator=self.handler.combined_authenticator, authenticator=self.handler.combined_authenticator,
auth_require_tls=False, # Allow AUTH over plain text (not recommended for production) auth_require_tls=False, # Allow AUTH over plain text (not recommended for production)
decode_data=True, decode_data=True,
hostname=self.hostname hostname=self.smtp_hostname # Use proper hostname for HELO
) )
+32
View File
@@ -0,0 +1,32 @@
[Unit]
Description=PyMTA Email Server
After=network.target
StartLimitIntervalSec=0
# check any errors when using this service:
# journalctl -u pymta-server.service -b -f
[Service]
Type=simple
User=appuser
Group=appuser
WorkingDirectory=/opt/PyMTA-server
Environment=PYTHONUNBUFFERED=1
ExecStart=/opt/PyMTA-server/.venv/bin/python main.py
Restart=always
RestartSec=5
StandardOutput=journal
StandardError=journal
# Security settings
# Capabilities for low ports < 1024 following 2 lines:
AmbientCapabilities=CAP_NET_BIND_SERVICE
CapabilityBoundingSet=CAP_NET_BIND_SERVICE
# if using port < 1024 comment out line bellow:
# NoNewPrivileges=true
PrivateTmp=true
ProtectSystem=strict
ReadWritePaths=/opt/PyMTA-server
ProtectHome=true
[Install]
WantedBy=multi-user.target
+5 -1
View File
@@ -1,4 +1,8 @@
# SMTP MTA server: # SMTP MTA server
# then you can allow run ports < 1024 with
# create env with `python -m venv .venv --copies` (This will copy the Python binary)
# for f in /opt/PyMTA-server/.venv/bin/python*; do sudo setcap 'cap_net_bind_service=+ep' "$f"; done
aiosmtpd aiosmtpd
sqlalchemy sqlalchemy
pyOpenSSL pyOpenSSL