From 3841426f5dfa26077b92f45830e868d2b57b2ff3 Mon Sep 17 00:00:00 2001 From: nahakubuilde Date: Sat, 31 May 2025 17:51:03 +0100 Subject: [PATCH] dkim deduplication in progress --- email_server/cli_tools.py | 48 +++++++++++++++++++++++-- email_server/dkim_manager.py | 24 ++++++++++++- email_server/models.py | 9 +++++ email_server/smtp_handler.py | 7 ++++ tests/email_header_received.txt | 64 --------------------------------- 5 files changed, 85 insertions(+), 67 deletions(-) delete mode 100644 tests/email_header_received.txt diff --git a/email_server/cli_tools.py b/email_server/cli_tools.py index 3ec7ecd..c488b23 100644 --- a/email_server/cli_tools.py +++ b/email_server/cli_tools.py @@ -3,7 +3,7 @@ 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.models import Session, Domain, User, WhitelistedIP, hash_password, create_tables, CustomHeader from email_server.dkim_manager import DKIMManager from email_server.tool_box import get_logger @@ -92,7 +92,7 @@ def add_whitelisted_ip(ip_address, domain_name): def generate_dkim_key(domain_name): """Generate DKIM key for a domain.""" dkim_manager = DKIMManager() - if dkim_manager.generate_dkim_keypair(domain_name): + if (dkim_manager.generate_dkim_keypair(domain_name)): print(f"Generated DKIM key for domain: {domain_name}") # Show DNS record @@ -149,6 +149,41 @@ def show_dns_records(): finally: session.close() +def add_custom_header(domain_name: str, header_name: str, header_value: str, is_active: bool = True) -> bool: + """Add a custom header for a domain. + + Args: + domain_name (str): The domain name. + header_name (str): The header name. + header_value (str): The header value. + is_active (bool): Whether the header is active. + + Returns: + bool: True if added, False otherwise. + """ + session = Session() + try: + domain = session.query(Domain).filter_by(domain_name=domain_name).first() + if not domain: + print(f"Domain {domain_name} not found") + return False + custom_header = CustomHeader( + domain_id=domain.id, + header_name=header_name, + header_value=header_value, + is_active=is_active + ) + session.add(custom_header) + session.commit() + print(f"Added custom header '{header_name}: {header_value}' for domain {domain_name}") + return True + except Exception as e: + session.rollback() + print(f"Error adding custom header: {e}") + return False + finally: + session.close() + def main(): """Main CLI function.""" parser = argparse.ArgumentParser(description="SMTP Server Management Tool") @@ -181,6 +216,12 @@ def main(): dns_parser = subparsers.add_parser('show-dns', help='Show DNS records for DKIM') + custom_header_parser = subparsers.add_parser('add-custom-header', help='Add a custom header for a domain') + custom_header_parser.add_argument('domain', help='Domain name') + custom_header_parser.add_argument('header_name', help='Header name') + custom_header_parser.add_argument('header_value', help='Header value') + custom_header_parser.add_argument('--inactive', action='store_true', help='Add as inactive (disabled)') + args = parser.parse_args() if not args.command: @@ -208,6 +249,9 @@ def main(): elif args.command == 'show-dns': show_dns_records() + + elif args.command == 'add-custom-header': + add_custom_header(args.domain, args.header_name, args.header_value, not args.inactive) if __name__ == '__main__': main() diff --git a/email_server/dkim_manager.py b/email_server/dkim_manager.py index 46a9091..076a8d1 100644 --- a/email_server/dkim_manager.py +++ b/email_server/dkim_manager.py @@ -6,7 +6,7 @@ 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.models import Session, Domain, DKIMKey, CustomHeader from email_server.settings_loader import load_settings from email_server.tool_box import get_logger import random @@ -211,3 +211,25 @@ class DKIMManager: logger.error(f"Error initializing default DKIM keys: {e}") finally: session.close() + + def get_active_custom_headers(self, domain_name: str) -> list: + """Get all active custom headers for a domain. + + Args: + domain_name (str): The domain name. + + Returns: + list: List of (header_name, header_value) tuples for active headers. + """ + session = Session() + try: + domain = session.query(Domain).filter_by(domain_name=domain_name).first() + if not domain: + return [] + headers = session.query(CustomHeader).filter_by(domain_id=domain.id, is_active=True).all() + return [(h.header_name, h.header_value) for h in headers] + except Exception as e: + logger.error(f"Error getting custom headers for {domain_name}: {e}") + return [] + finally: + session.close() diff --git a/email_server/models.py b/email_server/models.py index fca79e1..904aeac 100644 --- a/email_server/models.py +++ b/email_server/models.py @@ -71,6 +71,15 @@ class DKIMKey(Base): created_at = Column(DateTime, default=datetime.now) is_active = Column(Boolean, default=True) +class CustomHeader(Base): + """Model for storing custom headers per domain.""" + __tablename__ = 'custom_headers' + id = Column(Integer, primary_key=True) + domain_id = Column(Integer, nullable=False) + header_name = Column(String, nullable=False) + header_value = Column(String, nullable=False) + is_active = Column(Boolean, default=True) + def create_tables(): """Create all database tables.""" Base.metadata.create_all(engine) diff --git a/email_server/smtp_handler.py b/email_server/smtp_handler.py index 7a18612..032a77e 100644 --- a/email_server/smtp_handler.py +++ b/email_server/smtp_handler.py @@ -61,6 +61,13 @@ class CustomSMTPHandler: # Extract domain from sender for DKIM signing sender_domain = envelope.mail_from.split('@')[1] if '@' in envelope.mail_from else None + # Add custom headers before DKIM signing + if 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) signed_content = content dkim_signed = False diff --git a/tests/email_header_received.txt b/tests/email_header_received.txt deleted file mode 100644 index fea8d9c..0000000 --- a/tests/email_header_received.txt +++ /dev/null @@ -1,64 +0,0 @@ -Return-Path: -Delivered-To: michal@freebede.com -Received: from localhost (ip6-localhost [127.0.0.1]) - by mail.freebede.com (Haraka/3.0.3) with ESMTP id C9334594-05C2-4C2F-9929-8BC2EDE64F7B.1 - envelope-from ; - Sat, 31 May 2025 17:16:15 +0100 -X-Sieve: Pigeonhole Sieve 2.4.1-4+debian12 (0a86619f) -X-Sieve-Redirected-From: info@freebede.com -Delivered-To: info@freebede.com -Received-SPF: permerror (mail.freebede.com: permanent error in processing during lookup of Georgio@freebede.com: Unknown mechanism ipv4) - client-ip=217.154.124.184; -X-Haraka-FCrDNS: pymta.freebede.com -Received: from [217.154.124.184] (pymta.freebede.com [217.154.124.184]) - by mail.freebede.com (Haraka/3.0.3) with ESMTPS id A7500626-F3FD-4765-AC22-B223F4EBE9AF.1 - envelope-from - tls TLS_AES_256_GCM_SHA384; - Sat, 31 May 2025 17:16:14 +0100 -DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/simple; d=freebede.com; - i=@freebede.com; q=dns/txt; s=default; t=1748708170; h=from : to : - subject : date : message-id; - bh=d7U3WnzgqTDLdPM2GSrSAqnZP37FNND4SjOTtcrPPa8=; - b=k7YkS92hHtDtkiQMwQMVydweX+aeBTkrUXwcZNDkWcId1bItfSvK2sWhRorOOvt08z/+m - zButdzQ3Ia/1XPXFf9Ehkb3Jl5IRFnmrI2wgzN5jsJ5SY70zyrd6XVzWr64BbGDDhpnI+D/ - KXveMWeiJ66Ea++eEM8YG58StFGsMcVEyeiEL5WFaasPCsEPDaVTOWU+ietvpQg777tNDB2 - hAuJGCnGFngbqeV+6R1jiJM+TZX4KRp8HLPCkX0S52GRTcpNbI8diDY9pDizndasKZNvEEj - /YYX+P/vL+4wdUqjx2ALDjG0Z1G/381Rbkn3gWOCRzFpTq/v4ekRwwJAu5fA== -Date: Sat, 31 May 2025 17:16:09 +0100 -To: info@freebede.com -From: Georgio@freebede.com -Subject: TLS - Large body email -Message-Id: <20250531171609.046279@Supanone-PC> -X-Mailer: swaks v20240103.0 jetmore.org/john/code/swaks/ -MIME-Version: 1.0 -Content-Type: multipart/mixed; boundary="----=_MIME_BOUNDARY_000_46279" -X-Haraka-Karma: score: 4, good: 4, bad: 3, connections: 7, history: 1, asn_score: -77, asn_connections: 85, asn_good: 4, asn_bad: 81, awards: 132,281, fail:asn:history, rcpt_to -X-p0f-Result: os="Linux 2.2.x-3.x" link_type="Ethernet or modem" distance=10 total_conn=8 -X-Rspamd-Bar: ++ -X-Rspamd-Report: DMARC_POLICY_ALLOW(-0.5) HFILTER_HELO_BAREIP(3) MID_RHS_NOT_FQDN(0.5) MIME_GOOD(-0.1) R_DKIM_ALLOW(-0.2) ONCE_RECEIVED(0.2) R_SPF_ALLOW(-0.2) -X-Rspamd-Score: 2.699999 -DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=freebede.com; - h=Content-Type: MIME-Version: Message-Id: Subject: From: To: Date; - q=dns/txt; s=s20240310133; t=1748708175; - bh=d7U3WnzgqTDLdPM2GSrSAqnZP37FNND4SjOTtcrPPa8=; - b=I3qbjEWaV0amKMevlVUWA3tnoDLaZZCa5HX5YyFOwiFBpjhB2anvXnyE4U8864ER2bqN9VnRw - Gu5Ni4YjvNg4HVUABLTHNeTtEe7cik9N26mqjD2Xc6TUluzYtYjEpDGvxI6pfdRTAjFOE1KI0DT - OKqNppgzbGyChWFRZdd2PSe2d84OnLj9KXTtmwqNXFGz10EKvXsEQ+v/tX+JOkfE6HW/nAGmHrj - ZyZRMhzKPjm4SZHMy/T3MBGNeBwJHx8dg+9hNPKMvNMKFZp+ZzSbEKyD5bBBwWxVjxUlso0ZZZW - TzoOAVCqFJxLr4dK6qcpJJ9mjE/0mguJMdZ2J/ia0vBA== -Original-Authentication-Results: mail.freebede.com; - iprev=pass; - spf=permerror (mail.freebede.com: permanent error in processing during lookup of Georgio@freebede.com: Unknown mechanism ipv4) smtp.mailfrom=Georgio@freebede.com smtp.helo="[217.154.124.184]"; - dkim=pass header.i=@freebede.com header.s=default header.a=rsa-sha256 header.b=k7YkS92h; - dmarc=pass (p=REJECT arc=none) header.from=freebede.com header.d=freebede.com -X-Haraka-GeoIP-Received: 217.154.124.184:DE,217.154.124.184:DE -DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=freebede.com; - h=Content-Type: MIME-Version: Message-Id: Subject: From: To: Date; - q=dns/txt; s=s20240310133; t=1748708175; - bh=d7U3WnzgqTDLdPM2GSrSAqnZP37FNND4SjOTtcrPPa8=; - b=I3qbjEWaV0amKMevlVUWA3tnoDLaZZCa5HX5YyFOwiFBpjhB2anvXnyE4U8864ER2bqN9VnRw - Gu5Ni4YjvNg4HVUABLTHNeTtEe7cik9N26mqjD2Xc6TUluzYtYjEpDGvxI6pfdRTAjFOE1KI0DT - OKqNppgzbGyChWFRZdd2PSe2d84OnLj9KXTtmwqNXFGz10EKvXsEQ+v/tX+JOkfE6HW/nAGmHrj - ZyZRMhzKPjm4SZHMy/T3MBGNeBwJHx8dg+9hNPKMvNMKFZp+ZzSbEKyD5bBBwWxVjxUlso0ZZZW - TzoOAVCqFJxLr4dK6qcpJJ9mjE/0mguJMdZ2J/ia0vBA== -