moved
This commit is contained in:
284
email-worker/email_processing/rules_processor.py
Normal file
284
email-worker/email_processing/rules_processor.py
Normal file
@@ -0,0 +1,284 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Email rules processing (Auto-Reply/OOO and Forwarding)
|
||||
"""
|
||||
|
||||
import smtplib
|
||||
from email.mime.text import MIMEText
|
||||
from email.mime.multipart import MIMEMultipart
|
||||
from email.utils import parseaddr, formatdate, make_msgid
|
||||
from botocore.exceptions import ClientError
|
||||
|
||||
from logger import log
|
||||
from config import config, is_internal_address
|
||||
from aws.dynamodb_handler import DynamoDBHandler
|
||||
from aws.ses_handler import SESHandler
|
||||
from email_processing.parser import EmailParser
|
||||
|
||||
|
||||
class RulesProcessor:
|
||||
"""Processes email rules (OOO, Forwarding)"""
|
||||
|
||||
def __init__(self, dynamodb: DynamoDBHandler, ses: SESHandler):
|
||||
self.dynamodb = dynamodb
|
||||
self.ses = ses
|
||||
|
||||
def process_rules_for_recipient(
|
||||
self,
|
||||
recipient: str,
|
||||
parsed,
|
||||
domain: str,
|
||||
worker_name: str,
|
||||
metrics_callback=None
|
||||
):
|
||||
"""
|
||||
Process OOO and Forward rules for a recipient
|
||||
|
||||
Args:
|
||||
recipient: Recipient email address
|
||||
parsed: Parsed email message object
|
||||
domain: Email domain
|
||||
worker_name: Worker name for logging
|
||||
metrics_callback: Optional callback to increment metrics
|
||||
"""
|
||||
rule = self.dynamodb.get_email_rules(recipient)
|
||||
|
||||
if not rule:
|
||||
return
|
||||
|
||||
original_from = parsed.get('From', '')
|
||||
sender_name, sender_addr = parseaddr(original_from)
|
||||
if not sender_addr:
|
||||
sender_addr = original_from
|
||||
|
||||
# ============================================
|
||||
# OOO / Auto-Reply handling
|
||||
# ============================================
|
||||
if rule.get('ooo_active', False):
|
||||
self._handle_ooo(
|
||||
recipient,
|
||||
parsed,
|
||||
sender_addr,
|
||||
rule,
|
||||
domain,
|
||||
worker_name,
|
||||
metrics_callback
|
||||
)
|
||||
|
||||
# ============================================
|
||||
# Forward handling
|
||||
# ============================================
|
||||
forwards = rule.get('forwards', [])
|
||||
if forwards:
|
||||
self._handle_forwards(
|
||||
recipient,
|
||||
parsed,
|
||||
original_from,
|
||||
forwards,
|
||||
domain,
|
||||
worker_name,
|
||||
metrics_callback
|
||||
)
|
||||
|
||||
def _handle_ooo(
|
||||
self,
|
||||
recipient: str,
|
||||
parsed,
|
||||
sender_addr: str,
|
||||
rule: dict,
|
||||
domain: str,
|
||||
worker_name: str,
|
||||
metrics_callback=None
|
||||
):
|
||||
"""Handle Out-of-Office auto-reply"""
|
||||
# Don't reply to automatic messages
|
||||
auto_submitted = parsed.get('Auto-Submitted', '')
|
||||
precedence = (parsed.get('Precedence') or '').lower()
|
||||
|
||||
if auto_submitted and auto_submitted != 'no':
|
||||
log(f" ⏭ Skipping OOO for auto-submitted message", 'INFO', worker_name)
|
||||
return
|
||||
|
||||
if precedence in ['bulk', 'junk', 'list']:
|
||||
log(f" ⏭ Skipping OOO for {precedence} message", 'INFO', worker_name)
|
||||
return
|
||||
|
||||
if any(x in sender_addr.lower() for x in ['noreply', 'no-reply', 'mailer-daemon']):
|
||||
log(f" ⏭ Skipping OOO for noreply address", 'INFO', worker_name)
|
||||
return
|
||||
|
||||
try:
|
||||
ooo_msg = rule.get('ooo_message', 'I am out of office.')
|
||||
content_type = rule.get('ooo_content_type', 'text')
|
||||
ooo_reply = self._create_ooo_reply(parsed, recipient, ooo_msg, content_type)
|
||||
ooo_bytes = ooo_reply.as_bytes()
|
||||
|
||||
# Distinguish: Internal (Port 2525) vs External (SES)
|
||||
if is_internal_address(sender_addr):
|
||||
# Internal address → direct via Port 2525
|
||||
success = self._send_internal_email(recipient, sender_addr, ooo_bytes, worker_name)
|
||||
if success:
|
||||
log(f"✓ Sent OOO reply internally to {sender_addr}", 'SUCCESS', worker_name)
|
||||
else:
|
||||
log(f"⚠ Internal OOO reply failed to {sender_addr}", 'WARNING', worker_name)
|
||||
else:
|
||||
# External address → via SES
|
||||
success = self.ses.send_raw_email(recipient, sender_addr, ooo_bytes, worker_name)
|
||||
if success:
|
||||
log(f"✓ Sent OOO reply externally to {sender_addr} via SES", 'SUCCESS', worker_name)
|
||||
|
||||
if metrics_callback:
|
||||
metrics_callback('autoreply', domain)
|
||||
|
||||
except Exception as e:
|
||||
log(f"⚠ OOO reply failed to {sender_addr}: {e}", 'ERROR', worker_name)
|
||||
|
||||
def _handle_forwards(
|
||||
self,
|
||||
recipient: str,
|
||||
parsed,
|
||||
original_from: str,
|
||||
forwards: list,
|
||||
domain: str,
|
||||
worker_name: str,
|
||||
metrics_callback=None
|
||||
):
|
||||
"""Handle email forwarding"""
|
||||
for forward_to in forwards:
|
||||
try:
|
||||
fwd_msg = self._create_forward_message(parsed, recipient, forward_to, original_from)
|
||||
fwd_bytes = fwd_msg.as_bytes()
|
||||
|
||||
# Distinguish: Internal (Port 2525) vs External (SES)
|
||||
if is_internal_address(forward_to):
|
||||
# Internal address → direct via Port 2525 (no loop!)
|
||||
success = self._send_internal_email(recipient, forward_to, fwd_bytes, worker_name)
|
||||
if success:
|
||||
log(f"✓ Forwarded internally to {forward_to}", 'SUCCESS', worker_name)
|
||||
else:
|
||||
log(f"⚠ Internal forward failed to {forward_to}", 'WARNING', worker_name)
|
||||
else:
|
||||
# External address → via SES
|
||||
success = self.ses.send_raw_email(recipient, forward_to, fwd_bytes, worker_name)
|
||||
if success:
|
||||
log(f"✓ Forwarded externally to {forward_to} via SES", 'SUCCESS', worker_name)
|
||||
|
||||
if metrics_callback:
|
||||
metrics_callback('forward', domain)
|
||||
|
||||
except Exception as e:
|
||||
log(f"⚠ Forward failed to {forward_to}: {e}", 'ERROR', worker_name)
|
||||
|
||||
@staticmethod
|
||||
def _send_internal_email(from_addr: str, to_addr: str, raw_message: bytes, worker_name: str) -> bool:
|
||||
"""
|
||||
Send email via internal SMTP port (bypasses transport_maps)
|
||||
|
||||
Args:
|
||||
from_addr: From address
|
||||
to_addr: To address
|
||||
raw_message: Raw MIME message bytes
|
||||
worker_name: Worker name for logging
|
||||
|
||||
Returns:
|
||||
True on success, False on failure
|
||||
"""
|
||||
try:
|
||||
with smtplib.SMTP(config.smtp_host, config.internal_smtp_port, timeout=30) as conn:
|
||||
conn.ehlo()
|
||||
conn.sendmail(from_addr, [to_addr], raw_message)
|
||||
return True
|
||||
except Exception as e:
|
||||
log(f" ✗ Internal delivery failed to {to_addr}: {e}", 'ERROR', worker_name)
|
||||
return False
|
||||
|
||||
@staticmethod
|
||||
def _create_ooo_reply(original_parsed, recipient: str, ooo_msg: str, content_type: str = 'text'):
|
||||
"""Create Out-of-Office reply as complete MIME message"""
|
||||
text_body, html_body = EmailParser.extract_body_parts(original_parsed)
|
||||
original_subject = original_parsed.get('Subject', '(no subject)')
|
||||
original_from = original_parsed.get('From', 'unknown')
|
||||
|
||||
msg = MIMEMultipart('mixed')
|
||||
msg['From'] = recipient
|
||||
msg['To'] = original_from
|
||||
msg['Subject'] = f"Out of Office: {original_subject}"
|
||||
msg['Date'] = formatdate(localtime=True)
|
||||
msg['Message-ID'] = make_msgid(domain=recipient.split('@')[1])
|
||||
msg['In-Reply-To'] = original_parsed.get('Message-ID', '')
|
||||
msg['References'] = original_parsed.get('Message-ID', '')
|
||||
msg['Auto-Submitted'] = 'auto-replied'
|
||||
msg['X-SES-Worker-Processed'] = 'ooo-reply'
|
||||
|
||||
body_part = MIMEMultipart('alternative')
|
||||
|
||||
# Text version
|
||||
text_content = f"{ooo_msg}\n\n--- Original Message ---\n"
|
||||
text_content += f"From: {original_from}\n"
|
||||
text_content += f"Subject: {original_subject}\n\n"
|
||||
text_content += text_body
|
||||
body_part.attach(MIMEText(text_content, 'plain', 'utf-8'))
|
||||
|
||||
# HTML version (if desired and original available)
|
||||
if content_type == 'html' or html_body:
|
||||
html_content = f"<div>{ooo_msg}</div><br><hr><br>"
|
||||
html_content += "<strong>Original Message</strong><br>"
|
||||
html_content += f"<strong>From:</strong> {original_from}<br>"
|
||||
html_content += f"<strong>Subject:</strong> {original_subject}<br><br>"
|
||||
html_content += (html_body if html_body else text_body.replace('\n', '<br>'))
|
||||
body_part.attach(MIMEText(html_content, 'html', 'utf-8'))
|
||||
|
||||
msg.attach(body_part)
|
||||
return msg
|
||||
|
||||
@staticmethod
|
||||
def _create_forward_message(original_parsed, recipient: str, forward_to: str, original_from: str):
|
||||
"""Create Forward message as complete MIME message"""
|
||||
original_subject = original_parsed.get('Subject', '(no subject)')
|
||||
original_date = original_parsed.get('Date', 'unknown')
|
||||
|
||||
msg = MIMEMultipart('mixed')
|
||||
msg['From'] = recipient
|
||||
msg['To'] = forward_to
|
||||
msg['Subject'] = f"FWD: {original_subject}"
|
||||
msg['Date'] = formatdate(localtime=True)
|
||||
msg['Message-ID'] = make_msgid(domain=recipient.split('@')[1])
|
||||
msg['Reply-To'] = original_from
|
||||
msg['X-SES-Worker-Processed'] = 'forwarded'
|
||||
|
||||
text_body, html_body = EmailParser.extract_body_parts(original_parsed)
|
||||
body_part = MIMEMultipart('alternative')
|
||||
|
||||
# Text version
|
||||
fwd_text = "---------- Forwarded message ---------\n"
|
||||
fwd_text += f"From: {original_from}\n"
|
||||
fwd_text += f"Date: {original_date}\n"
|
||||
fwd_text += f"Subject: {original_subject}\n"
|
||||
fwd_text += f"To: {recipient}\n\n"
|
||||
fwd_text += text_body
|
||||
body_part.attach(MIMEText(fwd_text, 'plain', 'utf-8'))
|
||||
|
||||
# HTML version
|
||||
if html_body:
|
||||
fwd_html = "<div style='border-left:3px solid #ccc;padding-left:10px;'>"
|
||||
fwd_html += "<strong>---------- Forwarded message ---------</strong><br>"
|
||||
fwd_html += f"<strong>From:</strong> {original_from}<br>"
|
||||
fwd_html += f"<strong>Date:</strong> {original_date}<br>"
|
||||
fwd_html += f"<strong>Subject:</strong> {original_subject}<br>"
|
||||
fwd_html += f"<strong>To:</strong> {recipient}<br><br>"
|
||||
fwd_html += html_body
|
||||
fwd_html += "</div>"
|
||||
body_part.attach(MIMEText(fwd_html, 'html', 'utf-8'))
|
||||
|
||||
msg.attach(body_part)
|
||||
|
||||
# Copy attachments
|
||||
if original_parsed.is_multipart():
|
||||
for part in original_parsed.walk():
|
||||
if part.get_content_maintype() == 'multipart':
|
||||
continue
|
||||
if part.get_content_type() in ['text/plain', 'text/html']:
|
||||
continue
|
||||
msg.attach(part)
|
||||
|
||||
return msg
|
||||
Reference in New Issue
Block a user