update acc. to bounces

This commit is contained in:
Andreas Knuth 2025-12-19 17:12:40 -06:00
parent 6df8674b72
commit 1d1a384d1b
7 changed files with 838 additions and 177 deletions

137
bounces/5.4.1.json Normal file
View File

@ -0,0 +1,137 @@
{
"version": "0",
"id": "68eb43ad-3ad6-25ef-2b49-2389fc4460cc",
"detail-type": "Email Bounced",
"source": "aws.ses",
"account": "339712845857",
"time": "2025-12-19T02:24:37Z",
"region": "us-east-2",
"resources": [
"arn:aws:ses:us-east-2:339712845857:configuration-set/relay-outbound"
],
"detail": {
"eventType": "Bounce",
"bounce": {
"feedbackId": "010f019b346c64dc-ebd1959f-ac85-4d28-b2c2-e2db414889d2-000000",
"bounceType": "Permanent",
"bounceSubType": "General",
"bouncedRecipients": [
{
"emailAddress": "pishing@paypal.com",
"action": "failed",
"status": "5.0.0",
"diagnosticCode": "smtp; 5.1.0 - Unknown address error 550-'5.4.1 Recipient address rejected: Access denied. For more information see https: //aka.ms/EXOSmtpErrors [DS2PEPF00003441.namprd04.prod.outlook.com 2025-12-19T02:24:36.588Z 08DE3C04B3813774] (delivery attempts: 0)"
}
],
"timestamp": "2025-12-19T02:24:37.521Z",
"reportingMTA": "dns; mx2.paypalcorp.com"
},
"mail": {
"timestamp": "2025-12-19T02:24:34.082Z",
"source": "andreas.knuth@bayarea-cc.com",
"sourceArn": "arn:aws:ses:us-east-2:339712845857:identity/bayarea-cc.com",
"sendingAccountId": "339712845857",
"messageId": "010f019b346c5722-7f94b168-0d66-444c-8333-99f80801ee6e-000000",
"destination": [
"pishing@paypal.com"
],
"headersTruncated": False,
"headers": [
{
"name": "Received",
"value": "from mail.email-srvr.com (mail.email-srvr.com [2.56.188.138]) by email-smtp.amazonaws.com with SMTP (SimpleEmailService-d-4T8YRF3HF) id JWwKWtbMKwPcuMJWmawg for pishing@paypal.com; Fri, 19 Dec 2025 02:24:34 +0000 (UTC)"
},
{
"name": "DKIM-Signature",
"value": "v=1; a=rsa-sha256; c=relaxed/simple; d=bayarea-cc.com; s=mail; t=1766111073; bh=489KasDOSypdn6kagJw8c/vBfll20acGANR7WEnsNq8=; h=From:To:Subject:Reply-To:In-Reply-To:References; b=axFSO5cJaEy+bSCreaVfYY8ThHUvEAJmiVV26Qpw2sZG4YFoYglcNry2Gv2B+99ctJwcTAlxa/XzB0mJzzSpyU7WU0D03Kw/4k+8Mdl0mu+Li8icoINPJ0v5Kap2hVMRVp+ge6w7wAZR+rS46oAvL++piRZYr+85FGiHpFtJIK8e4a06sXtkHB4kDDNTDzKiTM7tTH6/oD4LV3LxeL29notQih5atTUOSo5LHN1QNp5Hq05A4sih7rM6J7CNKIouvqm1ku8I2+xUsgNu0neWnddBDV8njD24Gc70Flab22q5GDqVQ0caql7odpMlrCQjdmAgyEmeVP+JWjB3EnZ3DQ=="
},
{
"name": "Received",
"value": "from app.email-bayarea.com (roundcube-new.mail_network [172.18.0.5]) (Authenticated sender: andreas.knuth@bayarea-cc.com) by mail.email-srvr.com (Postfix) with ESMTPSA id 6CD2F2E60092 for <pishing@paypal.com>; Thu, 18 Dec 2025 20:24:33 -0600 (CST)"
},
{
"name": "MIME-Version",
"value": "1.0"
},
{
"name": "Date",
"value": "Thu, 18 Dec 2025 20:24:33 -0600"
},
{
"name": "From",
"value": "andreas.knuth@bayarea-cc.com"
},
{
"name": "To",
"value": "pishing@paypal.com"
},
{
"name": "Subject",
"value": "Fwd: A one-time merchant setup fee of $249.99 has been applied and will appear on your bank statement wit"
},
{
"name": "Reply-To",
"value": "andreas.knuth@bayarea-cc.com"
},
{
"name": "Mail-Reply-To",
"value": "andreas.knuth@bayarea-cc.com"
},
{
"name": "In-Reply-To",
"value": "<6061d865685c1bb406c127f32451d22d@bayarea-cc.com>"
},
{
"name": "References",
"value": "<boLgON9OSkmzVWOPwCp8qQ@geopod-ismtpd-45> <6061d865685c1bb406c127f32451d22d@bayarea-cc.com>"
},
{
"name": "Message-ID",
"value": "<bf937f16310bd1be5350425b2dfc3d65@bayarea-cc.com>"
},
{
"name": "X-Sender",
"value": "andreas.knuth@bayarea-cc.com"
},
{
"name": "Content-Type",
"value": "multipart/alternative; boundary='=_d6bdf41daf974c2c1b77e9250e4348a7'"
}
],
"commonHeaders": {
"from": [
"andreas.knuth@bayarea-cc.com"
],
"replyTo": [
"andreas.knuth@bayarea-cc.com"
],
"date": "Thu, 18 Dec 2025 20:24:33 -0600",
"to": [
"pishing@paypal.com"
],
"messageId": "010f019b346c5722-7f94b168-0d66-444c-8333-99f80801ee6e-000000",
"subject": "Fwd: A one-time merchant setup fee of $249.99 has been applied and will appear on your bank statement wit"
},
"tags": {
"ses:source-tls-version": [
"TLSv1.3"
],
"ses:operation": [
"SendSmtpEmail"
],
"ses:configuration-set": [
"relay-outbound"
],
"ses:source-ip": [
"2.56.188.138"
],
"ses:from-domain": [
"bayarea-cc.com"
],
"ses:caller-identity": [
"bizmatch.net"
]
}
}
}
}

125
bounces/ooo1.json Normal file
View File

@ -0,0 +1,125 @@
{
"version": "0",
"id": "b1198c79-d4df-6d77-a472-12c05eb99a39",
"detail-type": "Email Bounced",
"source": "aws.ses",
"account": "339712845857",
"time": "2025-12-19T01:59:01Z",
"region": "us-east-2",
"resources": [
"arn:aws:ses:us-east-2:339712845857:configuration-set/relay-outbound"
],
"detail": {
"eventType": "Bounce",
"bounce": {
"feedbackId": "010f019b3454f3b9-6b92ce4e-e1f2-420b-8dd3-e48e062f0f88-000000",
"bounceType": "Transient",
"bounceSubType": "General",
"bouncedRecipients": [
{
"emailAddress": "frankie@iitwelders.com"
}
],
"timestamp": "2025-12-19T01:59:01.245Z"
},
"mail": {
"timestamp": "2025-12-19T01:58:58.255Z",
"source": "andreas.knuth@bayarea-cc.com",
"sourceArn": "arn:aws:ses:us-east-2:339712845857:identity/bayarea-cc.com",
"sendingAccountId": "339712845857",
"messageId": "010f019b3454e7cf-36b8560d-7880-4913-9e5d-dd87f336b0dd-000000",
"destination": [
"frankie@iitwelders.com"
],
"headersTruncated": False,
"headers": [
{
"name": "Received",
"value": "from mail.email-srvr.com (mail.email-srvr.com [2.56.188.138]) by email-smtp.amazonaws.com with SMTP (SimpleEmailService-d-Z6YSX0FGF) id d7Quc01fG0CsS9eS7yfX for frankie@iitwelders.com; Fri, 19 Dec 2025 01:58:58 +0000 (UTC)"
},
{
"name": "DKIM-Signature",
"value": "v=1; a=rsa-sha256; c=relaxed/simple; d=bayarea-cc.com; s=mail; t=1766109537; bh=S/AVMjQHFbdT0GdJ56RlBKNMvace1V8iv+n0iBHTPYQ=; h=From:To:Subject:Reply-To; b=CX4lHSxen4aqQ5+3mlfl51hmyoK3mkP3gVu9mfILqPaxafH8aXNYfUYBxpRct9sQHNuN2OhgUfdjrTM/75WnKrV50wo13HeKw3D2b3d/N3zj447KG2eAGycm/guNibrcjhduLDERGVwMFaeWAAKHbbWfWnAw68yEFKkcnTCNB1imyAn9diDew5zO9q2ZuA0fOm3YXZ7qFmVtmmX4z6la0Rfa39gEM6wBiOhpZTtODyTqkmABFolVTEqc1VqYH27jB8ZVHi1bO4M42VGoRcDzvjOfkxq5ad/UQeho7HOsLuWnVG7H3BarTom/TdZYMrt2ZllH5N+nf2ec90/lH20CxA=="
},
{
"name": "Received",
"value": "from app.email-bayarea.com (roundcube-new.mail_network [172.18.0.5]) (Authenticated sender: andreas.knuth@bayarea-cc.com) by mail.email-srvr.com (Postfix) with ESMTPSA id EC1B02E5FD51 for <frankie@iitwelders.com>; Thu, 18 Dec 2025 19:58:56 -0600 (CST)"
},
{
"name": "MIME-Version",
"value": "1.0"
},
{
"name": "Date",
"value": "Thu, 18 Dec 2025 19:58:56 -0600"
},
{
"name": "From",
"value": "andreas.knuth@bayarea-cc.com"
},
{
"name": "To",
"value": "Frankie <frankie@iitwelders.com>"
},
{
"name": "Subject",
"value": "12/18/25 7:58"
},
{
"name": "Reply-To",
"value": "andreas.knuth@bayarea-cc.com"
},
{
"name": "Mail-Reply-To",
"value": "andreas.knuth@bayarea-cc.com"
},
{
"name": "Message-ID",
"value": "<17a781e80ecae12285697c536cc46033@bayarea-cc.com>"
},
{
"name": "X-Sender",
"value": "andreas.knuth@bayarea-cc.com"
},
{
"name": "Content-Type",
"value": "multipart/alternative; boundary='=_46eb06b0a62a2efa142c40c5eadbbc54'"
}
],
"commonHeaders": {
"from": [
"andreas.knuth@bayarea-cc.com"
],
"replyTo": [
"andreas.knuth@bayarea-cc.com"
],
"date": "Thu, 18 Dec 2025 19:58:56 -0600",
"to": [
"Frankie <frankie@iitwelders.com>"
],
"messageId": "010f019b3454e7cf-36b8560d-7880-4913-9e5d-dd87f336b0dd-000000",
"subject": "12/18/25 7:58"
},
"tags": {
"ses:source-tls-version": [
"TLSv1.3"
],
"ses:operation": [
"SendSmtpEmail"
],
"ses:configuration-set": [
"relay-outbound"
],
"ses:source-ip": [
"2.56.188.138"
],
"ses:from-domain": [
"bayarea-cc.com"
],
"ses:caller-identity": [
"bizmatch.net"
]
}
}
}
}

125
bounces/ooo2.json Normal file
View File

@ -0,0 +1,125 @@
{
"version": "0",
"id": "4d37ae3d-e411-2b83-8a83-6489a5fa1a00",
"detail-type": "Email Bounced",
"source": "aws.ses",
"account": "339712845857",
"time": "2025-12-19T02:10:33Z",
"region": "us-east-2",
"resources": [
"arn:aws:ses:us-east-2:339712845857:configuration-set/relay-outbound"
],
"detail": {
"eventType": "Bounce",
"bounce": {
"feedbackId": "010f019b345f8461-3382d3a0-42bb-4861-977f-e62606a24cb7-000000",
"bounceType": "Transient",
"bounceSubType": "General",
"bouncedRecipients": [
{
"emailAddress": "remote@gregknoppcpa.com"
}
],
"timestamp": "2025-12-19T02:10:33.636Z"
},
"mail": {
"timestamp": "2025-12-19T02:10:32.560Z",
"source": "andreas.knuth@bayarea-cc.com",
"sourceArn": "arn:aws:ses:us-east-2:339712845857:identity/bayarea-cc.com",
"sendingAccountId": "339712845857",
"messageId": "010f019b345f7ff0-e22c2d38-c499-48ed-8992-abbf1c44b6a1-000000",
"destination": [
"remote@gregknoppcpa.com"
],
"headersTruncated": False,
"headers": [
{
"name": "Received",
"value": "from mail.email-srvr.com (mail.email-srvr.com [2.56.188.138]) by email-smtp.amazonaws.com with SMTP (SimpleEmailService-d-V0JPVCFGF) id 6KbS70pRiY9lOcyjIONV for remote@gregknoppcpa.com; Fri, 19 Dec 2025 02:10:32 +0000 (UTC)"
},
{
"name": "DKIM-Signature",
"value": "v=1; a=rsa-sha256; c=relaxed/simple; d=bayarea-cc.com; s=mail; t=1766110231; bh=sU5OepBQM0PVwu+hgNjl2gP+fBXM9lfNeDiFo9j+0BQ=; h=From:To:Subject:Reply-To; b=lK1PWF722nu9AuCE0SRq7VBVHBrznhyiozlM2kxSSVFVUNHtV4abBKHMPdzE0c6oYN4blSogNMi9/qJA4EKSpoegMHertvETZpHHTM51M083wtzodojc5ZPKoOZjLpjWOVf3oqomccwUxTwqNXmyEdQcUH/lYz52o+b6GFFb7X7MkxQfA0VXgIYL5v0rIKszOoLAour3lfx99uoJSwIIVLZi4f5LFWa+FB48bGH67FaojHRqQzeioMQyLwa9fSKMG/bifT1/jPSmCauRPMSxzsdDBvk0nuVitr8/RgAno8FqfBH+UWJIw8Wt3gVQDLNL82hi5qWUgsXKwY3LFo2LkA=="
},
{
"name": "Received",
"value": "from app.email-bayarea.com (roundcube-new.mail_network [172.18.0.5]) (Authenticated sender: andreas.knuth@bayarea-cc.com) by mail.email-srvr.com (Postfix) with ESMTPSA id D9D3F2E5FD51 for <remote@gregknoppcpa.com>; Thu, 18 Dec 2025 20:10:31 -0600 (CST)"
},
{
"name": "MIME-Version",
"value": "1.0"
},
{
"name": "Date",
"value": "Thu, 18 Dec 2025 20:10:31 -0600"
},
{
"name": "From",
"value": "andreas.knuth@bayarea-cc.com"
},
{
"name": "To",
"value": "remote@gregknoppcpa.com"
},
{
"name": "Subject",
"value": "testing out-of-office messages"
},
{
"name": "Reply-To",
"value": "andreas.knuth@bayarea-cc.com"
},
{
"name": "Mail-Reply-To",
"value": "andreas.knuth@bayarea-cc.com"
},
{
"name": "Message-ID",
"value": "<95264ff6f55b9cc3ffcd451d6b27f7f0@bayarea-cc.com>"
},
{
"name": "X-Sender",
"value": "andreas.knuth@bayarea-cc.com"
},
{
"name": "Content-Type",
"value": "multipart/alternative; boundary='=_7ffce281e198378b2420ed61fd6b9156'"
}
],
"commonHeaders": {
"from": [
"andreas.knuth@bayarea-cc.com"
],
"replyTo": [
"andreas.knuth@bayarea-cc.com"
],
"date": "Thu, 18 Dec 2025 20:10:31 -0600",
"to": [
"remote@gregknoppcpa.com"
],
"messageId": "010f019b345f7ff0-e22c2d38-c499-48ed-8992-abbf1c44b6a1-000000",
"subject": "testing out-of-office messages"
},
"tags": {
"ses:source-tls-version": [
"TLSv1.3"
],
"ses:operation": [
"SendSmtpEmail"
],
"ses:configuration-set": [
"relay-outbound"
],
"ses:source-ip": [
"2.56.188.138"
],
"ses:from-domain": [
"bayarea-cc.com"
],
"ses:caller-identity": [
"bizmatch.net"
]
}
}
}
}

133
bounces/paypal.json Normal file
View File

@ -0,0 +1,133 @@
{
"version": "0",
"id": "ddfd563e-49f6-1f59-6d1e-c67158ab5eec",
"detail-type": "Email Bounced",
"source": "aws.ses",
"account": "339712845857",
"time": "2025-12-19T02:33:55Z",
"region": "us-east-2",
"resources": [
"arn:aws:ses:us-east-2:339712845857:configuration-set/relay-outbound"
],
"detail": {
"eventType": "Bounce",
"bounce": {
"feedbackId": "010f019b3474e821-12fa60c3-e47e-4289-a4b6-47ac55d996a2-000000",
"bounceType": "Undetermined",
"bounceSubType": "Undetermined",
"bouncedRecipients": [
{
"emailAddress": "phishing@paypal.com"
}
],
"timestamp": "2025-12-19T02:33:55.434Z"
},
"mail": {
"timestamp": "2025-12-19T02:33:53.244Z",
"source": "andreas.knuth@bayarea-cc.com",
"sourceArn": "arn:aws:ses:us-east-2:339712845857:identity/bayarea-cc.com",
"sendingAccountId": "339712845857",
"messageId": "010f019b3474df5c-c634e6cc-8ebb-4b13-957e-0e9b84e39917-000000",
"destination": [
"phishing@paypal.com"
],
"headersTruncated": False,
"headers": [
{
"name": "Received",
"value": "from mail.email-srvr.com (mail.email-srvr.com [2.56.188.138]) by email-smtp.amazonaws.com with SMTP (SimpleEmailService-d-V0JPVCFGF) id XSfVNEIjPhLtO2NEYG88 for phishing@paypal.com; Fri, 19 Dec 2025 02:33:53 +0000 (UTC)"
},
{
"name": "DKIM-Signature",
"value": "v=1; a=rsa-sha256; c=relaxed/simple; d=bayarea-cc.com; s=mail; t=1766111632; bh=ycI1TnY3sqcJF4JmY2LCeBTlZ8Zv+aR+7YbjD2Y1n0Y=; h=From:To:Subject:Reply-To:In-Reply-To:References; b=YQ/EtiYxQIi4Ykwx4ELKXP6gd5u+sev5/GnN97t2rkfxFjrGAZHFdUS9IHipOi/KG5VCAbW89ocW6vPZrdC9SpSxrxr+NMncceSBfvun7SgMQM7ja12clsMfOPebbLsp+TEoSwo43QW4IYsNJep8B7OTInTpadABgeiKd+yWe0BLfsa56tGr6OdIcCBKmxXm/qEZoEjkXooYWu0A5yWCrfpfpdvgZTKKaArturPAtiPUcQiUuDRx7jMkDQkofmBNTtrDbmaLzfEbPqfI2usavV7DCDpa70N6/fbVY2RgnFpcDYP3zd1gf4qDGdnsy9+8B848D1QV/HrEVDsh/Opoxw=="
},
{
"name": "Received",
"value": "from app.email-bayarea.com (roundcube-new.mail_network [172.18.0.5]) (Authenticated sender: andreas.knuth@bayarea-cc.com) by mail.email-srvr.com (Postfix) with ESMTPSA id 9685E2E60092 for <phishing@paypal.com>; Thu, 18 Dec 2025 20:33:52 -0600 (CST)"
},
{
"name": "MIME-Version",
"value": "1.0"
},
{
"name": "Date",
"value": "Thu, 18 Dec 2025 20:33:52 -0600"
},
{
"name": "From",
"value": "andreas.knuth@bayarea-cc.com"
},
{
"name": "To",
"value": "phishing@paypal.com"
},
{
"name": "Subject",
"value": "Fwd: A one-time merchant setup fee of $249.99 has been applied and will appear on your bank statement wit"
},
{
"name": "Reply-To",
"value": "andreas.knuth@bayarea-cc.com"
},
{
"name": "Mail-Reply-To",
"value": "andreas.knuth@bayarea-cc.com"
},
{
"name": "In-Reply-To",
"value": "<6061d865685c1bb406c127f32451d22d@bayarea-cc.com>"
},
{
"name": "References",
"value": "<boLgON9OSkmzVWOPwCp8qQ@geopod-ismtpd-45> <6061d865685c1bb406c127f32451d22d@bayarea-cc.com>"
},
{
"name": "Message-ID",
"value": "<e7ec8070400b953e735b6fbe5439fa1e@bayarea-cc.com>"
},
{
"name": "X-Sender",
"value": "andreas.knuth@bayarea-cc.com"
},
{
"name": "Content-Type",
"value": "multipart/alternative; boundary='=_eb88e98e1904b7ce5ebf2be21b8909fd'"
}
],
"commonHeaders": {
"from": [
"andreas.knuth@bayarea-cc.com"
],
"replyTo": [
"andreas.knuth@bayarea-cc.com"
],
"date": "Thu, 18 Dec 2025 20:33:52 -0600",
"to": [
"phishing@paypal.com"
],
"messageId": "010f019b3474df5c-c634e6cc-8ebb-4b13-957e-0e9b84e39917-000000",
"subject": "Fwd: A one-time merchant setup fee of $249.99 has been applied and will appear on your bank statement wit"
},
"tags": {
"ses:source-tls-version": [
"TLSv1.3"
],
"ses:operation": [
"SendSmtpEmail"
],
"ses:configuration-set": [
"relay-outbound"
],
"ses:source-ip": [
"2.56.188.138"
],
"ses:from-domain": [
"bayarea-cc.com"
],
"ses:caller-identity": [
"bizmatch.net"
]
}
}
}
}

View File

@ -1,74 +1,97 @@
import json
import boto3
import os
from datetime import datetime
dynamo = boto3.resource('dynamodb', region_name='us-east-2')
table = dynamo.Table('ses-outbound-messages')
# AWS Clients
s3 = boto3.client('s3')
sqs = boto3.client('sqs')
dynamodb = boto3.resource('dynamodb')
# DynamoDB Table
OUTBOUND_TABLE = os.environ.get('OUTBOUND_TABLE', 'ses-outbound-messages')
table = dynamodb.Table(OUTBOUND_TABLE)
def lambda_handler(event, context):
print(f"Received event: {event}")
"""
Verarbeitet SES Events:
- Bounce Events: Speichert bounce details in DynamoDB
- Send Events: Ignoriert (nicht mehr benötigt)
"""
detail = event.get('detail', {})
mail = detail.get('mail', {})
msg_id = mail.get('messageId')
print(f"Received event: {json.dumps(event)}")
if not msg_id:
print("No MessageId in event")
return
# SNS Wrapper entpacken
for record in event.get('Records', []):
if 'Sns' in record:
message = json.loads(record['Sns']['Message'])
else:
message = record
# Event-Type aus dem Event extrahieren
event_type = detail.get('eventType')
if event_type == 'Send':
source = mail.get('source')
destinations = mail.get('destination', [])
table.put_item(
Item={
'MessageId': msg_id,
'source': source,
'destinations': destinations,
'timestamp': mail.get('timestamp')
}
)
print(f"Stored SEND event for {msg_id}")
return
event_type = message.get('eventType')
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")
handle_bounce(message)
elif event_type == 'Send':
# Ignorieren - wird nicht mehr benötigt
print(f"Ignoring Send event (no longer needed)")
else:
print(f"Unknown event type: {event_type}")
return {'statusCode': 200}
def handle_bounce(message):
"""
Verarbeitet Bounce Events und speichert Details in DynamoDB
"""
try:
bounce = message.get('bounce', {})
mail = message.get('mail', {})
# Extrahiere relevante Daten
feedback_id = bounce.get('feedbackId') # Das ist die Message-ID!
bounce_type = bounce.get('bounceType', 'Unknown')
bounce_subtype = bounce.get('bounceSubType', 'Unknown')
bounced_recipients = [r['emailAddress'] for r in bounce.get('bouncedRecipients', [])]
timestamp = bounce.get('timestamp')
# Original Message Daten
original_source = mail.get('source')
original_message_id = mail.get('messageId')
if not feedback_id:
print(f"Warning: No feedbackId in bounce event")
return
table.update_item(
Key={'MessageId': msg_id},
UpdateExpression="ADD bouncedRecipients :b",
ExpressionAttributeValues={
':b': set(bounced)
print(f"Processing bounce: feedbackId={feedback_id}, type={bounce_type}/{bounce_subtype}")
print(f"Bounced recipients: {bounced_recipients}")
# Speichere in DynamoDB (feedback_id ist die Message-ID der Bounce-Mail!)
table.put_item(
Item={
'MessageId': feedback_id, # Primary Key
'original_message_id': original_message_id, # SES MessageId der Original-Mail
'original_source': original_source,
'bounceType': bounce_type,
'bounceSubType': bounce_subtype,
'bouncedRecipients': bounced_recipients, # Liste von Email-Adressen
'timestamp': timestamp or datetime.utcnow().isoformat(),
'event_type': 'bounce'
}
)
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
print(f"✓ Stored bounce info for feedbackId {feedback_id}")
table.update_item(
Key={'MessageId': msg_id},
UpdateExpression="ADD complaintRecipients :c",
ExpressionAttributeValues={
':c': set(complained)
}
)
print(f"Updated {msg_id} with complaintRecipients={complained}")
return
except Exception as e:
print(f"Error handling bounce: {e}")
import traceback
traceback.print_exc()
def handle_send(message):
"""
DEPRECATED - Wird nicht mehr benötigt
Send Events werden jetzt ignoriert
"""
pass

164
test_extract_v2.py Executable file
View File

@ -0,0 +1,164 @@
#!/usr/bin/env python3
"""
Test script für Message-ID Extraktion - VERBESSERTE VERSION
Kann lokal ausgeführt werden ohne AWS-Verbindung
"""
import re
from email.parser import BytesParser
from email.policy import SMTP as SMTPPolicy
def log(message: str, level: str = 'INFO'):
"""Dummy log für Tests"""
print(f"[{level}] {message}")
def extract_original_message_id(parsed):
"""
Extrahiert Original SES Message-ID aus Email
SES Format: 010f[hex32]-[hex8]-[hex4]-[hex4]-[hex4]-[hex12]-000000
"""
import re
# SES Message-ID Pattern (endet immer mit -000000)
ses_pattern = re.compile(r'010f[0-9a-f]{12}-[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}-000000')
# Die Message-ID der aktuellen Email (Bounce selbst) - diese wollen wir NICHT
current_msg_id = (parsed.get('Message-ID') or '').strip()
current_match = ses_pattern.search(current_msg_id)
current_id = current_match.group(0) if current_match else None
log(f"Current Message-ID: {current_id}", 'DEBUG')
# 1. Versuche Standard-Header (In-Reply-To, References)
for header in ['In-Reply-To', 'References']:
value = (parsed.get(header) or '').strip()
if value:
match = ses_pattern.search(value)
if match:
found_id = match.group(0)
# Nur nehmen wenn es NICHT die aktuelle Bounce-ID ist
if found_id != current_id:
log(f" Found Message-ID in {header}: {found_id}")
return found_id
# 2. Durchsuche den kompletten Email-Body (inkl. ALLE Attachments/Parts)
try:
body_text = ''
# Hole den kompletten Body als String
if parsed.is_multipart():
for part in parsed.walk():
content_type = part.get_content_type()
# SPEZIALFALL: message/rfc822 (eingebettete Messages)
if content_type == 'message/rfc822':
log(f" Processing embedded message/rfc822", 'DEBUG')
try:
# get_payload() gibt eine Liste mit einem EmailMessage-Objekt zurück!
payload = part.get_payload()
if isinstance(payload, list) and len(payload) > 0:
embedded_msg = payload[0]
# Hole Message-ID aus dem eingebetteten Message
embedded_id = (embedded_msg.get('Message-ID') or '').strip()
match = ses_pattern.search(embedded_id)
if match:
found_id = match.group(0)
log(f" Found ID in embedded msg: {found_id}", 'DEBUG')
# Nur nehmen wenn es NICHT die aktuelle Bounce-ID ist
if found_id != current_id:
log(f" ✓ Found Message-ID in embedded message: {found_id}")
return found_id
# Fallback: Konvertiere eingebettete Message zu String
body_text += embedded_msg.as_string()
except Exception as e:
log(f" Warning: Could not process embedded message: {e}", 'WARNING')
# Durchsuche ALLE anderen Parts (außer Binärdaten wie images)
elif content_type.startswith('text/') or content_type.startswith('application/'):
try:
payload = part.get_payload(decode=True)
if payload:
# Versuche als UTF-8, fallback auf Latin-1
try:
body_text += payload.decode('utf-8', errors='ignore')
except:
try:
body_text += payload.decode('latin-1', errors='ignore')
except:
# Letzter Versuch: als ASCII mit ignore
body_text += str(payload, errors='ignore')
except:
# Falls decode fehlschlägt, String-Payload holen
payload = part.get_payload()
if isinstance(payload, str):
body_text += payload
else:
# Nicht-Multipart Message
payload = parsed.get_payload(decode=True)
if payload:
try:
body_text = payload.decode('utf-8', errors='ignore')
except:
body_text = payload.decode('latin-1', errors='ignore')
# Suche alle SES Message-IDs im Body
matches = ses_pattern.findall(body_text)
if matches:
log(f" Found {len(matches)} total IDs in body: {matches}", 'DEBUG')
# Filtere die aktuelle Bounce-ID raus
candidates = [m for m in matches if m != current_id]
if candidates:
# Nehme die ERSTE der verbleibenden (meist die Original-ID)
log(f" Found {len(matches)} SES Message-ID(s) in body, using first (not bounce): {candidates[0]}")
return candidates[0]
else:
log(f" Found {len(matches)} SES Message-ID(s) but all match the bounce ID")
except Exception as e:
log(f" Warning: Could not search body for Message-ID: {e}", 'WARNING')
return None
def test_with_file(filepath: str):
"""Test mit einer echten Email-Datei"""
print(f"\n{'='*70}")
print(f"Testing: {filepath}")
print('='*70)
with open(filepath, 'rb') as f:
raw_bytes = f.read()
parsed = BytesParser(policy=SMTPPolicy).parsebytes(raw_bytes)
print(f"\nEmail Headers:")
print(f" From: {parsed.get('From')}")
print(f" To: {parsed.get('To')}")
print(f" Subject: {parsed.get('Subject')}")
print(f" Message-ID: {parsed.get('Message-ID')}")
print(f" In-Reply-To: {parsed.get('In-Reply-To')}")
print(f" References: {parsed.get('References')}")
print(f"\n--- EXTRACTION ---")
result = extract_original_message_id(parsed)
print(f"\n{'='*70}")
print(f"RESULT: {result}")
print('='*70)
return result
if __name__ == '__main__':
import sys
if len(sys.argv) > 1:
# Email-Datei als Argument
result = test_with_file(sys.argv[1])
# Exit code: 0 = success (ID found), 1 = failure (no ID)
sys.exit(0 if result else 1)
else:
print("Usage: python3 test_extract_v2.py <email_file>")
sys.exit(1)

160
worker.py
View File

@ -51,143 +51,97 @@ def get_bucket_name(domain):
"""Konvention: domain.tld -> domain-tld-emails"""
return domain.replace('.', '-') + '-emails'
def is_ses_bounce_or_autoreply(parsed):
"""Erkennt SES Bounces"""
def is_ses_bounce_notification(parsed):
"""
Prüft ob Email von SES MAILER-DAEMON ist
"""
from_h = (parsed.get('From') or '').lower()
auto_sub = (parsed.get('Auto-Submitted') or '').lower()
is_mailer_daemon = 'mailer-daemon@' in from_h and 'amazonses.com' in from_h
is_auto_replied = 'auto-replied' in auto_sub or 'auto-generated' in auto_sub
return is_mailer_daemon or is_auto_replied
return 'mailer-daemon@us-east-2.amazonses.com' in from_h
def extract_original_message_id(parsed):
def get_bounce_info_from_dynamodb(message_id):
"""
Extrahiert Original SES Message-ID aus Email
SES Format: 010f[hex32]-[hex8]-[hex4]-[hex4]-[hex4]-[hex12]-[hex6]
Sucht Bounce-Info in DynamoDB anhand der Message-ID
Returns: dict mit bounce info oder None
"""
import re
# SES Message-ID Pattern (endet immer mit -000000)
ses_pattern = re.compile(r'010f[0-9a-f]{12}-[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}-000000')
# 1. Versuche Standard-Header (In-Reply-To, References)
for header in ['In-Reply-To', 'References']:
value = (parsed.get(header) or '').strip()
if value:
match = ses_pattern.search(value)
if match:
log(f" Found Message-ID in {header}: {match.group(0)}")
return match.group(0)
# 2. Durchsuche Message-ID Header (manchmal steht dort die Original-ID)
msg_id_header = (parsed.get('Message-ID') or '').strip()
if msg_id_header:
match = ses_pattern.search(msg_id_header)
if match:
# Aber nur wenn es nicht die ID der aktuellen Bounce-Message ist
# (die beginnt oft auch mit 010f...)
pass # Wir überspringen das erstmal
# 3. Durchsuche den kompletten Email-Body (inkl. ALLE Attachments/Parts)
# Das fängt auch attached messages, text attachments, etc. ab
try:
body_text = ''
response = msg_table.get_item(Key={'MessageId': message_id})
item = response.get('Item')
# Hole den kompletten Body als String
if parsed.is_multipart():
for part in parsed.walk():
content_type = part.get_content_type()
if not item:
log(f"⚠ No bounce record found for Message-ID: {message_id}")
return None
# Durchsuche ALLE Parts (außer Binärdaten wie images)
# Text-Parts, HTML, attached messages, und auch application/* Parts
if content_type.startswith('text/') or \
content_type == 'message/rfc822' or \
content_type.startswith('application/'):
try:
payload = part.get_payload(decode=True)
if payload:
# Versuche als UTF-8, fallback auf Latin-1
try:
body_text += payload.decode('utf-8', errors='ignore')
except:
try:
body_text += payload.decode('latin-1', errors='ignore')
except:
# Letzter Versuch: als ASCII mit ignore
body_text += str(payload, errors='ignore')
except:
# Falls decode fehlschlägt, String-Payload holen
payload = part.get_payload()
if isinstance(payload, str):
body_text += payload
else:
# Nicht-Multipart Message
payload = parsed.get_payload(decode=True)
if payload:
try:
body_text = payload.decode('utf-8', errors='ignore')
except:
body_text = payload.decode('latin-1', errors='ignore')
# Suche alle SES Message-IDs im Body
matches = ses_pattern.findall(body_text)
if matches:
# Nehme die ERSTE gefundene ID (meist die Original-ID)
# Die letzte ist oft die Bounce-Message selbst
log(f" Found {len(matches)} SES Message-ID(s) in body, using first: {matches[0]}")
return matches[0]
return {
'original_source': item.get('original_source', ''),
'bounceType': item.get('bounceType', 'Unknown'),
'bounceSubType': item.get('bounceSubType', 'Unknown'),
'bouncedRecipients': item.get('bouncedRecipients', []),
'timestamp': item.get('timestamp', '')
}
except Exception as e:
log(f" Warning: Could not search body for Message-ID: {e}", 'WARNING')
log(f"⚠ DynamoDB Error: {e}", 'ERROR')
return None
def apply_bounce_logic(parsed, subject):
"""
Prüft auf Bounce, sucht in DynamoDB und schreibt Header um.
Prüft auf SES Bounce, sucht in DynamoDB und schreibt Header um.
Returns: (parsed_email_object, was_modified_bool)
"""
if not is_ses_bounce_or_autoreply(parsed):
if not is_ses_bounce_notification(parsed):
return parsed, False
log("🔍 Detected auto-response/bounce. Checking DynamoDB...")
original_msg_id = extract_original_message_id(parsed)
log("🔍 Detected SES MAILER-DAEMON bounce notification")
if not original_msg_id:
log("⚠ Could not extract original Message-ID")
# Message-ID aus Header extrahieren
message_id = (parsed.get('Message-ID') or '').strip('<>')
if not message_id:
log("⚠ Could not extract Message-ID from bounce notification")
return parsed, False
try:
log(f" Looking up Message-ID: {message_id}")
# Lookup in DynamoDB
result = msg_table.get_item(Key={'MessageId': original_msg_id})
item = result.get('Item')
bounce_info = get_bounce_info_from_dynamodb(message_id)
if not item:
log(f"⚠ No DynamoDB record found for {original_msg_id}")
if not bounce_info:
return parsed, False
# Treffer!
orig_source = item.get('source', '')
orig_destinations = item.get('destinations', [])
original_recipient = orig_destinations[0] if orig_destinations else ''
# Bounce Info ausgeben
original_source = bounce_info['original_source']
bounced_recipients = bounce_info['bouncedRecipients']
bounce_type = bounce_info['bounceType']
bounce_subtype = bounce_info['bounceSubType']
if original_recipient:
log(f"✓ Found original sender: {orig_source} -> intended for {original_recipient}")
log(f"✓ Found bounce info:")
log(f" Original sender: {original_source}")
log(f" Bounce type: {bounce_type}/{bounce_subtype}")
log(f" Bounced recipients: {bounced_recipients}")
# Nehme den ersten bounced recipient als neuen Absender
# (bei Multiple Recipients kann es mehrere geben)
if bounced_recipients:
new_from = bounced_recipients[0]
# Rewrite Headers
parsed['X-Original-SES-From'] = parsed.get('From', '')
parsed.replace_header('From', original_recipient)
parsed['X-Bounce-Type'] = f"{bounce_type}/{bounce_subtype}"
parsed.replace_header('From', new_from)
if not parsed.get('Reply-To'):
parsed['Reply-To'] = original_recipient
parsed['Reply-To'] = new_from
if 'delivery status notification' in subject.lower():
parsed.replace_header('Subject', f"Delivery Status: {original_recipient}")
# Subject anpassen
if 'delivery status notification' in subject.lower() or 'thanks for your submission' in subject.lower():
parsed.replace_header('Subject', f"Delivery Status: {new_from}")
log(f"✓ Rewritten FROM: {new_from}")
return parsed, True
except Exception as e:
log(f"⚠ DynamoDB Error: {e}")
log("⚠ No bounced recipients found in bounce info")
return parsed, False
def signal_handler(signum, frame):