From e8e8739d07e01fcdbe6a864618f0260560fb4080 Mon Sep 17 00:00:00 2001 From: Andreas Knuth Date: Wed, 17 Dec 2025 15:45:37 -0600 Subject: [PATCH] claudi-fix --- worker.py | 227 ++++++++++++++++++++++++++++++++++++++++++------------ 1 file changed, 179 insertions(+), 48 deletions(-) diff --git a/worker.py b/worker.py index d9db188..ce27c16 100755 --- a/worker.py +++ b/worker.py @@ -414,29 +414,159 @@ def send_email(from_addr: str, recipient: str, raw_message: bytes) -> tuple: log(f" ✗ {recipient}: Connection error - {e}", 'ERROR') return False, str(e), False -def extract_body(parsed): - """Extrahiert den Body als String, handhabt multipart und priorisiert text/plain oder text/html.""" - body = '' +def extract_body_parts(parsed): + """ + 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(): for part in parsed.walk(): - if part.get_content_type() == 'text/plain': + content_type = part.get_content_type() + + if content_type == 'text/plain': 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: 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: - 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: log(f"⚠ Error decoding text/html part: {e}", 'WARNING') else: 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: 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"
{ooo_msg}



" + html_content += "
" + html_content += f"Original Message
" + html_content += f"From: {original_from}
" + html_content += f"Subject: {original_subject}

" + html_content += (html_body if html_body else text_body.replace('\n', '
')) + html_content += "
" + 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 = "
" + fwd_html += "---------- Forwarded message ---------
" + fwd_html += f"From: {original_from}
" + fwd_html += f"Date: {original_date}
" + fwd_html += f"Subject: {original_subject}
" + fwd_html += f"To: {recipient}

" + fwd_html += html_body + fwd_html += "
" + 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 @@ -543,7 +673,7 @@ def process_message(message_body: dict, receive_count: int) -> bool: original_body = extract_body(parsed) # 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: try: 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 if rule.get('ooo_active', False): ooo_msg = rule.get('ooo_message', 'Default OOO message.') - content_type = rule.get('ooo_content_type', 'text') # Default: text - sender = parsed.get('From') # Original-Sender + content_type = rule.get('ooo_content_type', 'text') + sender = parsed.get('From') - reply_subject = f"Out of Office: {subject}" - - if content_type == 'html': - reply_body = {'Html': {'Data': f"

{ooo_msg}


Original Message:
Subject: {parsed.get('Subject')}
From: {sender}

{original_body}
"}} - else: - reply_body = {'Text': {'Data': f"{ooo_msg}\n\nOriginal Message:\nSubject: {parsed.get('Subject')}\nFrom: {sender}\n\n{original_body}"}} - - ses.send_email( - Source=recipient, # Verifizierte eigene Adresse - Destination={'ToAddresses': [sender]}, - Message={ - 'Subject': {'Data': reply_subject}, - 'Body': reply_body # Dynamisch Text oder Html - }, - ReplyToAddresses=[recipient] # Optional: Für Replies - ) - log(f"✓ Sent OOO reply to {sender} from {recipient}") + try: + # Erstelle komplette MIME-Message + ooo_reply = create_ooo_reply(parsed, recipient, ooo_msg, content_type) + + # Sende via send_raw_email (unterstützt komplexe MIME) + ses.send_raw_email( + Source=recipient, + Destinations=[sender], + RawMessage={'Data': ooo_reply.as_bytes()} + ) + log(f"✓ Sent OOO reply to {sender} from {recipient}") + + except ClientError as e: + error_code = e.response['Error']['Code'] + log(f"⚠ SES OOO send failed ({error_code}): {e}", 'ERROR') # Forward handling forwards = rule.get('forwards', []) if forwards: - original_from = parsed.get('From') # Für Headers - 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 + original_from = parsed.get('From') for forward_to in forwards: - ses.send_email( - Source=recipient, # Verifizierte eigene Adresse - Destination={'ToAddresses': [forward_to]}, - Message={ - 'Subject': {'Data': fwd_subject}, - 'Body': fwd_body - }, - ReplyToAddresses=[original_from] # Original-Sender für Replies - ) - log(f"✓ Forwarded to {forward_to} from {recipient} (original: {original_from})") + try: + # Erstelle komplette Forward-Message mit Attachments + fwd_msg = create_forward_message(parsed, recipient, forward_to, original_from) + + # Sende via send_raw_email + ses.send_raw_email( + Source=recipient, + Destinations=[forward_to], + RawMessage={'Data': fwd_msg.as_bytes()} + ) + 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'] 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': - log(f"⚠ SES AccessDenied for {recipient}: {e}. Check IAM policy.", 'ERROR') + log(f"⚠ SES AccessDenied for {recipient}: Check IAM policy.", 'ERROR') else: log(f"⚠ SES error for {recipient}: {e}", 'ERROR') 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) log(f"📤 Sending to {len(recipients)} recipient(s)...")