diff --git a/lambda_function.py b/lambda_function.py index 64ffc63..484763e 100644 --- a/lambda_function.py +++ b/lambda_function.py @@ -11,10 +11,21 @@ sqs = boto3.client('sqs', region_name='us-east-2') # AWS Region AWS_REGION = 'us-east-2' +# Dynamo Table +dynamo = boto3.resource('dynamodb', region_name=AWS_REGION) +msg_table = dynamo.Table('ses-outbound-messages') + # Metadata Keys PROCESSED_KEY = 'processed' PROCESSED_VALUE = 'true' +def is_ses_autoresponse(parsed): + from_h = (parsed.get('From') or '').lower() + auto_sub = (parsed.get('Auto-Submitted') or '').lower() + return ( + 'mailer-daemon@us-east-2.amazonses.com' in from_h + and 'auto-replied' in auto_sub + ) def domain_to_bucket(domain: str) -> str: """Konvertiert Domain zu S3 Bucket Namen""" @@ -323,19 +334,91 @@ def lambda_handler(event, context): # E-Mail laden um Subject zu extrahieren subject = '(unknown)' + raw_bytes = b'' + parsed = None + modified = False + try: print(f"\n📖 Reading email for metadata...") obj = s3.get_object(Bucket=bucket, Key=key) raw_bytes = obj['Body'].read() - - # Nur Headers parsen (schneller) + metadata = obj.get('Metadata', {}) or {} + + # Header parsen parsed = BytesParser(policy=SMTPPolicy).parsebytes(raw_bytes) subject = parsed.get('subject', '(no subject)') - print(f" Subject: {subject}") - + + # 🔁 SES Auto-Response erkennen + if is_ses_autoresponse(parsed): + print(" Detected SES auto-response (out-of-office)") + + # Message-ID der ursprünglichen Mail aus In-Reply-To / References holen + in_reply_to = (parsed.get('In-Reply-To') or '').strip() + if not in_reply_to: + refs = (parsed.get('References') or '').strip() + # nimm die erste ID aus References + in_reply_to = refs.split()[0] if refs else '' + + lookup_id = '' + if in_reply_to.startswith('<') and '>' in in_reply_to: + lookup_id = in_reply_to[1:in_reply_to.find('>')] + else: + lookup_id = in_reply_to + + original = None + if lookup_id: + try: + res = msg_table.get_item(Key={'MessageId': lookup_id}) + original = res.get('Item') + print(f" Dynamo lookup for {lookup_id}: {'hit' if original else 'miss'}") + except Exception as e: + print(f"⚠ Dynamo lookup failed: {e}") + + if original: + orig_from = original.get('source', '') + destinations = original.get('destinations', []) or [] + # einfache Variante: nimm den ersten Empfänger + orig_to = destinations[0] if destinations else '' + + # Domain hast du oben bereits aus recipients[0] extrahiert + display = f"Out of Office from {orig_to}" if orig_to else "Out of Office" + + # ursprüngliche Infos sichern + parsed['X-SES-Original-From'] = parsed.get('From', '') + parsed['X-SES-Original-Recipient'] = orig_to + + # From für den User "freundlich" machen + parsed.replace_header('From', f'"{display}" ') + + # Antworten trotzdem an den Absender deiner ursprünglichen Mail + if orig_from: + parsed['Reply-To'] = orig_from + + subj = parsed.get('Subject', 'out of office') + if not subj.lower().startswith('out of office'): + parsed.replace_header('Subject', f"Out of office: {subj}") + + # geänderte Mail zurück in Bytes + raw_bytes = parsed.as_bytes() + modified = True + print(" Auto-response rewritten for delivery to user inbox") + else: + print(" No original send record found for auto-response") + + # Wenn wir die Mail verändert haben, aktualisieren wir das S3-Objekt + if modified: + s3.put_object( + Bucket=bucket, + Key=key, + Body=raw_bytes, + Metadata=metadata + ) + print(" Updated S3 object with rewritten auto-response") + except Exception as e: print(f" ⚠ Could not parse email (continuing): {e}") + # In Queue einreihen (EINE Message mit ALLEN Recipients) try: diff --git a/lambda_function_outbound.py b/lambda_function_outbound.py new file mode 100644 index 0000000..c67cd55 --- /dev/null +++ b/lambda_function_outbound.py @@ -0,0 +1,72 @@ +import boto3 +import os + +dynamo = boto3.resource('dynamodb', region_name='us-east-2') +table = dynamo.Table('ses-outbound-messages') + +def lambda_handler(event, context): + detail = event['detail'] + mail = detail['mail'] + msg_id = mail['messageId'] + source = mail['source'] + destinations = mail['destination'] # Liste + + if not msg_id: + print("No MessageId in event") + return + + if event_type == 'SEND': + # wie bisher + source = mail.get('source') + destinations = mail.get('destination', []) + table.put_item( + Item={ + 'MessageId': msg_id, + 'source': source, + 'destinations': destinations, + 'timestamp': mail.get('timestamp') + } + ) + return + + if event_type == 'BOUNCE': + bounce = detail.get('bounce', {}) + bounced = [ + r.get('emailAddress') + for r in bounce.get('bouncedRecipients', []) + if r.get('emailAddress') + ] + if not bounced: + print("No bouncedRecipients in bounce event") + return + + # in DynamoDB anhängen + table.update_item( + Key={'MessageId': msg_id}, + UpdateExpression="ADD bouncedRecipients :b", + ExpressionAttributeValues={ + ':b': set(bounced) # String-Set + } + ) + print(f"Updated {msg_id} with bouncedRecipients={bounced}") + return + + if event_type == 'COMPLAINT': + complaint = detail.get('complaint', {}) + complained = [ + r.get('emailAddress') + for r in complaint.get('complainedRecipients', []) + if r.get('emailAddress') + ] + if not complained: + return + + table.update_item( + Key={'MessageId': msg_id}, + UpdateExpression="ADD complaintRecipients :c", + ExpressionAttributeValues={ + ':c': set(complained) + } + ) + print(f"Updated {msg_id} with complaintRecipients={complained}") + return