claudi-fix

This commit is contained in:
Andreas Knuth 2025-12-17 15:45:37 -06:00
parent 3c59075bd1
commit e8e8739d07
1 changed files with 179 additions and 48 deletions

227
worker.py
View File

@ -414,29 +414,159 @@ def send_email(from_addr: str, recipient: str, raw_message: bytes) -> tuple:
log(f"{recipient}: Connection error - {e}", 'ERROR') log(f"{recipient}: Connection error - {e}", 'ERROR')
return False, str(e), False return False, str(e), False
def extract_body(parsed): def extract_body_parts(parsed):
"""Extrahiert den Body als String, handhabt multipart und priorisiert text/plain oder text/html.""" """
body = '' Extrahiert sowohl text/plain als auch text/html Body-Parts.
Returns: (text_body: str, html_body: str or None)
"""
text_body = ''
html_body = None
if parsed.is_multipart(): if parsed.is_multipart():
for part in parsed.walk(): for part in parsed.walk():
if part.get_content_type() == 'text/plain': content_type = part.get_content_type()
if content_type == 'text/plain':
try: try:
body += part.get_payload(decode=True).decode('utf-8', errors='ignore') + '\n' text_body += part.get_payload(decode=True).decode('utf-8', errors='ignore')
except Exception as e: except Exception as e:
log(f"⚠ Error decoding text/plain part: {e}", 'WARNING') log(f"⚠ Error decoding text/plain part: {e}", 'WARNING')
elif part.get_content_type() == 'text/html' and not body: # Fallback zu HTML, wenn kein Plain
elif content_type == 'text/html':
try: try:
body += part.get_payload(decode=True).decode('utf-8', errors='ignore') + '\n' html_body = part.get_payload(decode=True).decode('utf-8', errors='ignore')
except Exception as e: except Exception as e:
log(f"⚠ Error decoding text/html part: {e}", 'WARNING') log(f"⚠ Error decoding text/html part: {e}", 'WARNING')
else: else:
try: try:
body = parsed.get_payload(decode=True).decode('utf-8', errors='ignore') payload = parsed.get_payload(decode=True)
if payload:
decoded = payload.decode('utf-8', errors='ignore')
if parsed.get_content_type() == 'text/html':
html_body = decoded
else:
text_body = decoded
except Exception as e: except Exception as e:
log(f"⚠ Error decoding non-multipart body: {e}", 'WARNING') log(f"⚠ Error decoding non-multipart body: {e}", 'WARNING')
body = str(parsed.get_payload()) # Fallback zu raw String text_body = str(parsed.get_payload())
return text_body.strip() if text_body else '(No body content)', html_body
def create_ooo_reply(original_parsed, recipient, ooo_msg, content_type='text'):
"""
Erstellt eine Out-of-Office Reply als komplette MIME-Message.
Behält Original-Body (text + html) bei.
"""
from email.mime.multipart import MIMEMultipart
from email.mime.text import MIMEText
from email.utils import formatdate, make_msgid
text_body, html_body = extract_body_parts(original_parsed)
original_subject = original_parsed.get('Subject', '(no subject)')
original_from = original_parsed.get('From', 'unknown')
# Neue Message erstellen
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' # Verhindert Loops
# Body-Teil erstellen
body_part = MIMEMultipart('alternative')
# Text-Version
text_content = f"{ooo_msg}\n\n"
text_content += "--- 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 (wenn gewünscht und Original vorhanden)
if content_type == 'html' or html_body:
html_content = f"<div>{ooo_msg}</div><br><hr><br>"
html_content += "<blockquote style='margin:10px 0;padding:10px;border-left:3px solid #ccc;'>"
html_content += f"<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>'))
html_content += "</blockquote>"
body_part.attach(MIMEText(html_content, 'html', 'utf-8'))
msg.attach(body_part)
return msg
def create_forward_message(original_parsed, recipient, forward_to, original_from):
"""
Erstellt eine Forward-Message als komplette MIME-Message.
Behält ALLE Original-Parts inkl. Attachments bei.
"""
from email.mime.multipart import MIMEMultipart
from email.mime.text import MIMEText
from email.utils import formatdate, make_msgid
original_subject = original_parsed.get('Subject', '(no subject)')
original_date = original_parsed.get('Date', 'unknown')
# Neue Message erstellen
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
# Forward-Header als Text
text_body, html_body = extract_body_parts(original_parsed)
# Body-Teil
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;margin:10px 0;'>"
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)
# WICHTIG: Attachments kopieren
if original_parsed.is_multipart():
for part in original_parsed.walk():
# Nur non-body parts (Attachments)
if part.get_content_maintype() == 'multipart':
continue
if part.get_content_type() in ['text/plain', 'text/html']:
continue # Body bereits oben behandelt
# Attachment hinzufügen
msg.attach(part)
return msg
return body.strip() if body else '(No body content)'
# ========================================== # ==========================================
# HAUPTFUNKTION: PROCESS MESSAGE # HAUPTFUNKTION: PROCESS MESSAGE
@ -543,7 +673,7 @@ def process_message(message_body: dict, receive_count: int) -> bool:
original_body = extract_body(parsed) original_body = extract_body(parsed)
# 5. OOO & FORWARD LOGIC (neu, vor SMTP-Versand) # 5. OOO & FORWARD LOGIC (neu, vor SMTP-Versand)
if rules_table and not is_ses_bounce_or_autoreply(parsed): # Vermeide Loops bei Bounces/Auto-Replies if rules_table and not is_ses_bounce_or_autoreply(parsed):
for recipient in recipients: for recipient in recipients:
try: try:
rule = rules_table.get_item(Key={'email_address': recipient}).get('Item', {}) rule = rules_table.get_item(Key={'email_address': recipient}).get('Item', {})
@ -551,57 +681,58 @@ def process_message(message_body: dict, receive_count: int) -> bool:
# OOO handling # OOO handling
if rule.get('ooo_active', False): if rule.get('ooo_active', False):
ooo_msg = rule.get('ooo_message', 'Default OOO message.') ooo_msg = rule.get('ooo_message', 'Default OOO message.')
content_type = rule.get('ooo_content_type', 'text') # Default: text content_type = rule.get('ooo_content_type', 'text')
sender = parsed.get('From') # Original-Sender sender = parsed.get('From')
reply_subject = f"Out of Office: {subject}" try:
# Erstelle komplette MIME-Message
if content_type == 'html': ooo_reply = create_ooo_reply(parsed, recipient, ooo_msg, content_type)
reply_body = {'Html': {'Data': f"<p>{ooo_msg}</p><br><blockquote>Original Message:<br>Subject: {parsed.get('Subject')}<br>From: {sender}<br><br>{original_body}</blockquote>"}}
else: # Sende via send_raw_email (unterstützt komplexe MIME)
reply_body = {'Text': {'Data': f"{ooo_msg}\n\nOriginal Message:\nSubject: {parsed.get('Subject')}\nFrom: {sender}\n\n{original_body}"}} ses.send_raw_email(
Source=recipient,
ses.send_email( Destinations=[sender],
Source=recipient, # Verifizierte eigene Adresse RawMessage={'Data': ooo_reply.as_bytes()}
Destination={'ToAddresses': [sender]}, )
Message={ log(f"✓ Sent OOO reply to {sender} from {recipient}")
'Subject': {'Data': reply_subject},
'Body': reply_body # Dynamisch Text oder Html except ClientError as e:
}, error_code = e.response['Error']['Code']
ReplyToAddresses=[recipient] # Optional: Für Replies log(f"⚠ SES OOO send failed ({error_code}): {e}", 'ERROR')
)
log(f"✓ Sent OOO reply to {sender} from {recipient}")
# Forward handling # Forward handling
forwards = rule.get('forwards', []) forwards = rule.get('forwards', [])
if forwards: if forwards:
original_from = parsed.get('From') # Für Headers original_from = parsed.get('From')
fwd_subject = f"FWD: {subject}"
fwd_body_text = f"Forwarded from: {original_from}\n\n{original_body}"
fwd_body = {'Text': {'Data': fwd_body_text}} # Erweiterbar auf HTML
for forward_to in forwards: for forward_to in forwards:
ses.send_email( try:
Source=recipient, # Verifizierte eigene Adresse # Erstelle komplette Forward-Message mit Attachments
Destination={'ToAddresses': [forward_to]}, fwd_msg = create_forward_message(parsed, recipient, forward_to, original_from)
Message={
'Subject': {'Data': fwd_subject}, # Sende via send_raw_email
'Body': fwd_body ses.send_raw_email(
}, Source=recipient,
ReplyToAddresses=[original_from] # Original-Sender für Replies Destinations=[forward_to],
) RawMessage={'Data': fwd_msg.as_bytes()}
log(f"✓ Forwarded to {forward_to} from {recipient} (original: {original_from})") )
log(f"✓ Forwarded to {forward_to} from {recipient} (original: {original_from})")
except ClientError as e:
error_code = e.response['Error']['Code']
log(f"⚠ SES forward failed to {forward_to} ({error_code}): {e}", 'ERROR')
except ClientError as e: # Fix: Korrekter Exception-Typ except ClientError as e:
error_code = e.response['Error']['Code'] error_code = e.response['Error']['Code']
if error_code == 'MessageRejected': if error_code == 'MessageRejected':
log(f"⚠ SES rejected send for {recipient}: {e}. Check verification or quotas.", 'ERROR') log(f"⚠ SES rejected send for {recipient}: Check verification/quotas.", 'ERROR')
elif error_code == 'AccessDenied': elif error_code == 'AccessDenied':
log(f"⚠ SES AccessDenied for {recipient}: {e}. Check IAM policy.", 'ERROR') log(f"⚠ SES AccessDenied for {recipient}: Check IAM policy.", 'ERROR')
else: else:
log(f"⚠ SES error for {recipient}: {e}", 'ERROR') log(f"⚠ SES error for {recipient}: {e}", 'ERROR')
except Exception as e: except Exception as e:
log(f"⚠ General error for {recipient}: {e}", 'WARNING') log(f"⚠ Rule processing error for {recipient}: {e}", 'WARNING')
traceback.print_exc()
# 6. SMTP VERSAND (Loop über Recipients) # 6. SMTP VERSAND (Loop über Recipients)
log(f"📤 Sending to {len(recipients)} recipient(s)...") log(f"📤 Sending to {len(recipients)} recipient(s)...")