updates
This commit is contained in:
parent
deed33c0cf
commit
5122082914
|
|
@ -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}")
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -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"
|
||||
|
|
|
|||
Loading…
Reference in New Issue