claudi-fix
This commit is contained in:
parent
3c59075bd1
commit
e8e8739d07
227
worker.py
227
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')
|
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)...")
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue