#!/usr/bin/env python3 """ Postfix Content Filter for Internal Email Processing Handles forwarding and auto-reply for local deliveries """ import os import sys import smtplib import logging from email import message_from_binary_file from email.mime.text import MIMEText from email.utils import parseaddr, formatdate, make_msgid # Setup logging logging.basicConfig( level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s', handlers=[ logging.FileHandler('/var/log/mail/content_filter.log'), logging.StreamHandler(sys.stderr) ] ) # AWS Configuration AWS_REGION = 'us-east-2' DYNAMODB_RULES_TABLE = 'email-rules' # SMTP Configuration REINJECT_HOST = 'localhost' REINJECT_PORT = 10026 # Initialize boto3 (lazy import to catch errors) try: import boto3 dynamodb = boto3.resource('dynamodb', region_name=AWS_REGION) rules_table = dynamodb.Table(DYNAMODB_RULES_TABLE) DYNAMODB_AVAILABLE = True logging.info("DynamoDB connection initialized") except Exception as e: DYNAMODB_AVAILABLE = False logging.error(f"DynamoDB initialization failed: {e}") def get_email_rules(email_address): """Fetch forwarding and auto-reply rules from DynamoDB""" if not DYNAMODB_AVAILABLE: return {} 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)}") return item except Exception as e: logging.error(f"DynamoDB error for {email_address}: {e}") return {} def should_send_autoreply(sender_addr): """Check if we should send auto-reply to this sender""" sender_lower = sender_addr.lower() # Don't reply to automated senders blocked_patterns = [ 'mailer-daemon', 'postmaster', 'noreply', 'no-reply', 'donotreply', 'bounce', 'amazonses.com' ] for pattern in blocked_patterns: if pattern in sender_lower: logging.info(f"Skipping auto-reply to automated sender: {sender_addr}") return False return True def send_autoreply(original_msg, recipient_rules, recipient_addr): """Send auto-reply if enabled""" if not recipient_rules.get('ooo_active'): return sender = original_msg.get('From') if not sender: logging.warning("No sender address, skipping auto-reply") return # Extract email from "Name " format sender_name, sender_addr = parseaddr(sender) if not should_send_autoreply(sender_addr): return subject = original_msg.get('Subject', 'No Subject') message_id = original_msg.get('Message-ID') # Get auto-reply message ooo_message = recipient_rules.get('ooo_message', 'I am currently unavailable.') content_type = recipient_rules.get('ooo_content_type', 'text') # Create auto-reply if content_type == 'html': from email.mime.multipart import MIMEMultipart from email.mime.text import MIMEText as MIMETextPart reply = MIMEMultipart('alternative') reply.attach(MIMETextPart(ooo_message, 'plain')) reply.attach(MIMETextPart(ooo_message, 'html')) else: reply = MIMEText(ooo_message, 'plain', 'utf-8') reply['From'] = recipient_addr reply['To'] = sender_addr reply['Subject'] = f"Automatic Reply: {subject}" reply['Date'] = formatdate(localtime=True) reply['Message-ID'] = make_msgid() reply['Auto-Submitted'] = 'auto-replied' # RFC 3834 reply['Precedence'] = 'bulk' if message_id: reply['In-Reply-To'] = message_id reply['References'] = message_id # Send via local SMTP try: with smtplib.SMTP(REINJECT_HOST, REINJECT_PORT, timeout=30) as smtp: smtp.send_message(reply) logging.info(f"✓ Sent auto-reply from {recipient_addr} to {sender_addr}") except Exception as e: logging.error(f"✗ Auto-reply failed: {e}") def send_forwards(original_msg_bytes, recipient_rules, recipient_addr): """Forward email to configured addresses""" forwards = recipient_rules.get('forwards', []) if not forwards: return 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 msg['X-Forwarded-For'] = recipient_addr msg['X-Original-To'] = recipient_addr # Send via local SMTP with smtplib.SMTP(REINJECT_HOST, REINJECT_PORT, timeout=30) as smtp: smtp.sendmail( from_addr=recipient_addr, to_addrs=[forward_addr], msg=msg.as_bytes() ) logging.info(f"✓ Forwarded from {recipient_addr} to {forward_addr}") except Exception as e: logging.error(f"✗ Forward to {forward_addr} failed: {e}") def main(): """Main content filter logic""" if len(sys.argv) < 3: logging.error("Usage: content_filter.py [recipient2] ...") sys.exit(1) sender = sys.argv[1] recipients = sys.argv[2:] logging.info(f"Processing email from {sender} to {', '.join(recipients)}") # 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 # Process each recipient for recipient in recipients: try: rules = get_email_rules(recipient) if rules: # Send auto-reply if configured send_autoreply(msg, rules, recipient) # Send forwards if configured send_forwards(msg_bytes, rules, recipient) else: logging.debug(f"No rules for {recipient}") except Exception as e: logging.error(f"Error processing rules for {recipient}: {e}") # Re-inject original email for normal delivery 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)}") sys.exit(0) except Exception as e: logging.error(f"✗ Delivery failed: {e}") sys.exit(75) # EX_TEMPFAIL - Postfix will retry if __name__ == '__main__': try: main() except Exception as e: logging.error(f"Fatal error: {e}") sys.exit(75)