dkim deduplication in progress
This commit is contained in:
@@ -3,7 +3,7 @@ Command-line tools for managing the SMTP server.
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
import argparse
|
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.dkim_manager import DKIMManager
|
||||||
from email_server.tool_box import get_logger
|
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):
|
def generate_dkim_key(domain_name):
|
||||||
"""Generate DKIM key for a domain."""
|
"""Generate DKIM key for a domain."""
|
||||||
dkim_manager = DKIMManager()
|
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}")
|
print(f"Generated DKIM key for domain: {domain_name}")
|
||||||
|
|
||||||
# Show DNS record
|
# Show DNS record
|
||||||
@@ -149,6 +149,41 @@ def show_dns_records():
|
|||||||
finally:
|
finally:
|
||||||
session.close()
|
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():
|
def main():
|
||||||
"""Main CLI function."""
|
"""Main CLI function."""
|
||||||
parser = argparse.ArgumentParser(description="SMTP Server Management Tool")
|
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')
|
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()
|
args = parser.parse_args()
|
||||||
|
|
||||||
if not args.command:
|
if not args.command:
|
||||||
@@ -209,5 +250,8 @@ def main():
|
|||||||
elif args.command == 'show-dns':
|
elif args.command == 'show-dns':
|
||||||
show_dns_records()
|
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__':
|
if __name__ == '__main__':
|
||||||
main()
|
main()
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import dkim
|
|||||||
from cryptography.hazmat.primitives import serialization, hashes
|
from cryptography.hazmat.primitives import serialization, hashes
|
||||||
from cryptography.hazmat.primitives.asymmetric import rsa
|
from cryptography.hazmat.primitives.asymmetric import rsa
|
||||||
from datetime import datetime
|
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.settings_loader import load_settings
|
||||||
from email_server.tool_box import get_logger
|
from email_server.tool_box import get_logger
|
||||||
import random
|
import random
|
||||||
@@ -211,3 +211,25 @@ class DKIMManager:
|
|||||||
logger.error(f"Error initializing default DKIM keys: {e}")
|
logger.error(f"Error initializing default DKIM keys: {e}")
|
||||||
finally:
|
finally:
|
||||||
session.close()
|
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()
|
||||||
|
|||||||
@@ -71,6 +71,15 @@ class DKIMKey(Base):
|
|||||||
created_at = Column(DateTime, default=datetime.now)
|
created_at = Column(DateTime, default=datetime.now)
|
||||||
is_active = Column(Boolean, default=True)
|
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():
|
def create_tables():
|
||||||
"""Create all database tables."""
|
"""Create all database tables."""
|
||||||
Base.metadata.create_all(engine)
|
Base.metadata.create_all(engine)
|
||||||
|
|||||||
@@ -61,6 +61,13 @@ class CustomSMTPHandler:
|
|||||||
# 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
|
||||||
|
|
||||||
|
# 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)
|
# Relay the email (all modifications done)
|
||||||
signed_content = content
|
signed_content = content
|
||||||
dkim_signed = False
|
dkim_signed = False
|
||||||
|
|||||||
@@ -1,64 +0,0 @@
|
|||||||
Return-Path: <Georgio@freebede.com>
|
|
||||||
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 <Georgio@freebede.com>;
|
|
||||||
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 <Georgio@freebede.com>
|
|
||||||
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==
|
|
||||||
|
|
||||||
Reference in New Issue
Block a user