This commit is contained in:
Andreas Knuth 2026-01-16 21:53:34 -06:00
parent deed33c0cf
commit 5122082914
6 changed files with 47 additions and 25 deletions

View File

@ -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 <email>" 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}")

View File

@ -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

View File

@ -1,3 +1,3 @@
# Content Filter Configuration
# Routes all local deliveries through content filter on port 10025
content_filter = smtp:[localhost]:10025
# Use transport_maps for selective filtering (only locals)
transport_maps = regexp:/etc/postfix/local_transport_maps

View File

@ -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

View File

@ -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

View File

@ -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"