diff --git a/DMS/content_filter.py b/DMS/content_filter.py index a81d5a8..03f36a7 100644 --- a/DMS/content_filter.py +++ b/DMS/content_filter.py @@ -11,6 +11,8 @@ import logging from email import message_from_binary_file from email.mime.text import MIMEText from email.utils import parseaddr, formatdate, make_msgid +from datetime import datetime, timedelta +from io import BytesIO # Setup logging logging.basicConfig( @@ -30,6 +32,10 @@ DYNAMODB_RULES_TABLE = 'email-rules' REINJECT_HOST = 'localhost' REINJECT_PORT = 10026 +# Cache for DynamoDB rules (TTL 5 minutes) +RULES_CACHE = {} +CACHE_TTL = timedelta(minutes=5) + # Initialize boto3 (lazy import to catch errors) try: import boto3 @@ -42,21 +48,28 @@ except Exception as e: logging.error(f"DynamoDB initialization failed: {e}") def get_email_rules(email_address): - """Fetch forwarding and auto-reply rules from DynamoDB""" + """Fetch forwarding and auto-reply rules from DynamoDB with caching""" if not DYNAMODB_AVAILABLE: return {} + now = datetime.now() + cache_key = email_address.lower() + if cache_key in RULES_CACHE and now - RULES_CACHE[cache_key]['time'] < CACHE_TTL: + logging.debug(f"Cache hit for {email_address}") + return RULES_CACHE[cache_key]['rules'] + try: response = rules_table.get_item(Key={'email_address': email_address}) item = response.get('Item', {}) if item: logging.info(f"Rules found for {email_address}: forwards={len(item.get('forwards', []))}, ooo={item.get('ooo_active', False)}") + RULES_CACHE[cache_key] = {'rules': item, 'time': now} return item except Exception as e: logging.error(f"DynamoDB error for {email_address}: {e}") return {} -def should_send_autoreply(sender_addr): +def should_send_autoreply(original_msg, sender_addr): """Check if we should send auto-reply to this sender""" sender_lower = sender_addr.lower() @@ -76,6 +89,11 @@ def should_send_autoreply(sender_addr): logging.info(f"Skipping auto-reply to automated sender: {sender_addr}") return False + # Check for auto-submitted header to prevent loops (RFC 3834) + if original_msg.get('Auto-Submitted', '').startswith('auto-'): + logging.info(f"Skipping auto-reply due to Auto-Submitted header: {sender_addr}") + return False + return True def send_autoreply(original_msg, recipient_rules, recipient_addr): @@ -91,7 +109,7 @@ def send_autoreply(original_msg, recipient_rules, recipient_addr): # Extract email from "Name " format sender_name, sender_addr = parseaddr(sender) - if not should_send_autoreply(sender_addr): + if not should_send_autoreply(original_msg, sender_addr): return subject = original_msg.get('Subject', 'No Subject') @@ -140,7 +158,6 @@ def send_forwards(original_msg_bytes, recipient_rules, recipient_addr): for forward_addr in forwards: try: # Parse message again for clean forwarding - from io import BytesIO msg = message_from_binary_file(BytesIO(original_msg_bytes)) # Add forwarding headers @@ -167,17 +184,12 @@ def main(): sender = sys.argv[1] recipients = sys.argv[2:] - logging.info(f"Processing email from {sender} to {', '.join(recipients)}") + logging.info(f"Processing email from {sender} to {', '.join(recipient)}") # Read email from stdin try: msg_bytes = sys.stdin.buffer.read() - msg = message_from_binary_file(sys.stdin.buffer) - - # Parse again from bytes for processing - from io import BytesIO msg = message_from_binary_file(BytesIO(msg_bytes)) - except Exception as e: logging.error(f"Failed to read email: {e}") sys.exit(75) # EX_TEMPFAIL @@ -185,6 +197,13 @@ def main(): # Process each recipient for recipient in recipients: try: + # Check if mail is internal (same domain) - skip if external + _, recipient_domain = parseaddr(recipient)[1].rsplit('@', 1) if '@' in recipient else ('', '') + _, sender_domain = parseaddr(sender)[1].rsplit('@', 1) if '@' in sender else ('', '') + if recipient_domain != sender_domain: + logging.info(f"Skipping external mail to {recipient} (sender domain: {sender_domain})") + continue + rules = get_email_rules(recipient) if rules: @@ -203,7 +222,7 @@ def main(): try: with smtplib.SMTP(REINJECT_HOST, REINJECT_PORT, timeout=30) as smtp: smtp.sendmail(sender, recipients, msg_bytes) - logging.info(f"✓ Delivered to {', '.join(recipients)}") + logging.info(f"✓ Delivered to {', '.join(recipient)}") sys.exit(0) except Exception as e: logging.error(f"✗ Delivery failed: {e}") diff --git a/DMS/docker-data/dms/config/postfix/local_transport_maps b/DMS/docker-data/dms/config/postfix/local_transport_maps new file mode 100644 index 0000000..7bbfeac --- /dev/null +++ b/DMS/docker-data/dms/config/postfix/local_transport_maps @@ -0,0 +1,4 @@ +# Filter only local/internal deliveries (adjust to your domains) +/^.*@example\.com$/ smtp:[localhost]:10025 # Replace with your domains, e.g. /^.*@andreasknuth\.de$/ +/^.*@another-domain\.com$/ smtp:[localhost]:10025 +# Add more lines for additional domains from your setup \ No newline at end of file diff --git a/DMS/docker-data/dms/config/postfix/main.cf.append b/DMS/docker-data/dms/config/postfix/main.cf.append index 14c7b86..bb5f67b 100644 --- a/DMS/docker-data/dms/config/postfix/main.cf.append +++ b/DMS/docker-data/dms/config/postfix/main.cf.append @@ -1,3 +1,3 @@ # Content Filter Configuration -# Routes all local deliveries through content filter on port 10025 -content_filter = smtp:[localhost]:10025 \ No newline at end of file +# Use transport_maps for selective filtering (only locals) +transport_maps = regexp:/etc/postfix/local_transport_maps \ No newline at end of file diff --git a/DMS/docker-data/dms/config/postfix/master.cf.append b/DMS/docker-data/dms/config/postfix/master.cf.append index 5036d89..51d8088 100644 --- a/DMS/docker-data/dms/config/postfix/master.cf.append +++ b/DMS/docker-data/dms/config/postfix/master.cf.append @@ -2,7 +2,6 @@ # Content Filter Setup # Two additional SMTP services for content filtering # - # Port 10025: Content filter input # Receives mail from main Postfix, passes to content_filter.py localhost:10025 inet n - n - - smtpd @@ -17,7 +16,6 @@ localhost:10025 inet n - n - - smtpd -o mynetworks=127.0.0.0/8 -o smtpd_authorized_xforward_hosts=127.0.0.0/8 -o receive_override_options=no_unknown_recipient_checks - # Port 10026: Content filter output (re-injection) # Receives processed mail from content_filter.py for final delivery localhost:10026 inet n - n - - smtpd diff --git a/DMS/docker-data/dms/config/postfix/transport b/DMS/docker-data/dms/config/postfix/transport deleted file mode 100644 index 5d091e4..0000000 --- a/DMS/docker-data/dms/config/postfix/transport +++ /dev/null @@ -1,10 +0,0 @@ -outlook.com smtp:[email-smtp.us-east-2.amazonaws.com]:587 -.outlook.com smtp:[email-smtp.us-east-2.amazonaws.com]:587 -live.com smtp:[email-smtp.us-east-2.amazonaws.com]:587 -.live.com smtp:[email-smtp.us-east-2.amazonaws.com]:587 -msn.com smtp:[email-smtp.us-east-2.amazonaws.com]:587 -.msn.com smtp:[email-smtp.us-east-2.amazonaws.com]:587 -hotmail.com smtp:[email-smtp.us-east-2.amazonaws.com]:587 -.hotmail.com smtp:[email-smtp.us-east-2.amazonaws.com]:587 -iitwelders.com smtp:[email-smtp.us-east-2.amazonaws.com]:587 -.iitwelderstp:[email-smtp.us-east-2.amazonaws.com]:587 \ No newline at end of file diff --git a/DMS/docker-data/dms/config/user-patches.sh b/DMS/docker-data/dms/config/user-patches.sh index f3089f8..f9360e0 100644 --- a/DMS/docker-data/dms/config/user-patches.sh +++ b/DMS/docker-data/dms/config/user-patches.sh @@ -29,6 +29,17 @@ else echo "[user-patches.sh] ⚠ master.cf.append not found, skipping" fi +# NEW: Create and postmap local_transport_maps for selective filtering +echo "[user-patches.sh] Creating local_transport_maps..." +install -D -m 0644 /dev/null "$DST_DIR/local_transport_maps" +cat > "$DST_DIR/local_transport_maps" << 'EOF' +# Filter only local/internal deliveries (adjust to your domains) +/^.*@example\.com$/ smtp:[localhost]:10025 # Replace with your domains, e.g. /^.*@andreasknuth\.de$/ +/^.*@another-domain\.com$/ smtp:[localhost]:10025 +EOF +postmap "$DST_DIR/local_transport_maps" +echo "[user-patches.sh] ✓ local_transport_maps created and mapped" + # Verify content filter script exists and is executable if [ -x "/usr/local/bin/content_filter.py" ]; then echo "[user-patches.sh] ✓ Content filter script found"