updates
This commit is contained in:
@@ -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}")
|
||||
|
||||
Reference in New Issue
Block a user