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 import message_from_binary_file
|
||||||
from email.mime.text import MIMEText
|
from email.mime.text import MIMEText
|
||||||
from email.utils import parseaddr, formatdate, make_msgid
|
from email.utils import parseaddr, formatdate, make_msgid
|
||||||
|
from datetime import datetime, timedelta
|
||||||
|
from io import BytesIO
|
||||||
|
|
||||||
# Setup logging
|
# Setup logging
|
||||||
logging.basicConfig(
|
logging.basicConfig(
|
||||||
|
|
@ -30,6 +32,10 @@ DYNAMODB_RULES_TABLE = 'email-rules'
|
||||||
REINJECT_HOST = 'localhost'
|
REINJECT_HOST = 'localhost'
|
||||||
REINJECT_PORT = 10026
|
REINJECT_PORT = 10026
|
||||||
|
|
||||||
|
# Cache for DynamoDB rules (TTL 5 minutes)
|
||||||
|
RULES_CACHE = {}
|
||||||
|
CACHE_TTL = timedelta(minutes=5)
|
||||||
|
|
||||||
# Initialize boto3 (lazy import to catch errors)
|
# Initialize boto3 (lazy import to catch errors)
|
||||||
try:
|
try:
|
||||||
import boto3
|
import boto3
|
||||||
|
|
@ -42,21 +48,28 @@ except Exception as e:
|
||||||
logging.error(f"DynamoDB initialization failed: {e}")
|
logging.error(f"DynamoDB initialization failed: {e}")
|
||||||
|
|
||||||
def get_email_rules(email_address):
|
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:
|
if not DYNAMODB_AVAILABLE:
|
||||||
return {}
|
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:
|
try:
|
||||||
response = rules_table.get_item(Key={'email_address': email_address})
|
response = rules_table.get_item(Key={'email_address': email_address})
|
||||||
item = response.get('Item', {})
|
item = response.get('Item', {})
|
||||||
if item:
|
if item:
|
||||||
logging.info(f"Rules found for {email_address}: forwards={len(item.get('forwards', []))}, ooo={item.get('ooo_active', False)}")
|
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
|
return item
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logging.error(f"DynamoDB error for {email_address}: {e}")
|
logging.error(f"DynamoDB error for {email_address}: {e}")
|
||||||
return {}
|
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"""
|
"""Check if we should send auto-reply to this sender"""
|
||||||
sender_lower = sender_addr.lower()
|
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}")
|
logging.info(f"Skipping auto-reply to automated sender: {sender_addr}")
|
||||||
return False
|
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
|
return True
|
||||||
|
|
||||||
def send_autoreply(original_msg, recipient_rules, recipient_addr):
|
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
|
# Extract email from "Name <email>" format
|
||||||
sender_name, sender_addr = parseaddr(sender)
|
sender_name, sender_addr = parseaddr(sender)
|
||||||
|
|
||||||
if not should_send_autoreply(sender_addr):
|
if not should_send_autoreply(original_msg, sender_addr):
|
||||||
return
|
return
|
||||||
|
|
||||||
subject = original_msg.get('Subject', 'No Subject')
|
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:
|
for forward_addr in forwards:
|
||||||
try:
|
try:
|
||||||
# Parse message again for clean forwarding
|
# Parse message again for clean forwarding
|
||||||
from io import BytesIO
|
|
||||||
msg = message_from_binary_file(BytesIO(original_msg_bytes))
|
msg = message_from_binary_file(BytesIO(original_msg_bytes))
|
||||||
|
|
||||||
# Add forwarding headers
|
# Add forwarding headers
|
||||||
|
|
@ -167,17 +184,12 @@ def main():
|
||||||
sender = sys.argv[1]
|
sender = sys.argv[1]
|
||||||
recipients = sys.argv[2:]
|
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
|
# Read email from stdin
|
||||||
try:
|
try:
|
||||||
msg_bytes = sys.stdin.buffer.read()
|
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))
|
msg = message_from_binary_file(BytesIO(msg_bytes))
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logging.error(f"Failed to read email: {e}")
|
logging.error(f"Failed to read email: {e}")
|
||||||
sys.exit(75) # EX_TEMPFAIL
|
sys.exit(75) # EX_TEMPFAIL
|
||||||
|
|
@ -185,6 +197,13 @@ def main():
|
||||||
# Process each recipient
|
# Process each recipient
|
||||||
for recipient in recipients:
|
for recipient in recipients:
|
||||||
try:
|
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)
|
rules = get_email_rules(recipient)
|
||||||
|
|
||||||
if rules:
|
if rules:
|
||||||
|
|
@ -203,7 +222,7 @@ def main():
|
||||||
try:
|
try:
|
||||||
with smtplib.SMTP(REINJECT_HOST, REINJECT_PORT, timeout=30) as smtp:
|
with smtplib.SMTP(REINJECT_HOST, REINJECT_PORT, timeout=30) as smtp:
|
||||||
smtp.sendmail(sender, recipients, msg_bytes)
|
smtp.sendmail(sender, recipients, msg_bytes)
|
||||||
logging.info(f"✓ Delivered to {', '.join(recipients)}")
|
logging.info(f"✓ Delivered to {', '.join(recipient)}")
|
||||||
sys.exit(0)
|
sys.exit(0)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logging.error(f"✗ Delivery failed: {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
|
# Content Filter Configuration
|
||||||
# Routes all local deliveries through content filter on port 10025
|
# Use transport_maps for selective filtering (only locals)
|
||||||
content_filter = smtp:[localhost]:10025
|
transport_maps = regexp:/etc/postfix/local_transport_maps
|
||||||
|
|
@ -2,7 +2,6 @@
|
||||||
# Content Filter Setup
|
# Content Filter Setup
|
||||||
# Two additional SMTP services for content filtering
|
# Two additional SMTP services for content filtering
|
||||||
#
|
#
|
||||||
|
|
||||||
# Port 10025: Content filter input
|
# Port 10025: Content filter input
|
||||||
# Receives mail from main Postfix, passes to content_filter.py
|
# Receives mail from main Postfix, passes to content_filter.py
|
||||||
localhost:10025 inet n - n - - smtpd
|
localhost:10025 inet n - n - - smtpd
|
||||||
|
|
@ -17,7 +16,6 @@ localhost:10025 inet n - n - - smtpd
|
||||||
-o mynetworks=127.0.0.0/8
|
-o mynetworks=127.0.0.0/8
|
||||||
-o smtpd_authorized_xforward_hosts=127.0.0.0/8
|
-o smtpd_authorized_xforward_hosts=127.0.0.0/8
|
||||||
-o receive_override_options=no_unknown_recipient_checks
|
-o receive_override_options=no_unknown_recipient_checks
|
||||||
|
|
||||||
# Port 10026: Content filter output (re-injection)
|
# Port 10026: Content filter output (re-injection)
|
||||||
# Receives processed mail from content_filter.py for final delivery
|
# Receives processed mail from content_filter.py for final delivery
|
||||||
localhost:10026 inet n - n - - smtpd
|
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"
|
echo "[user-patches.sh] ⚠ master.cf.append not found, skipping"
|
||||||
fi
|
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
|
# Verify content filter script exists and is executable
|
||||||
if [ -x "/usr/local/bin/content_filter.py" ]; then
|
if [ -x "/usr/local/bin/content_filter.py" ]; then
|
||||||
echo "[user-patches.sh] ✓ Content filter script found"
|
echo "[user-patches.sh] ✓ Content filter script found"
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue