diff --git a/bounces/5.4.1.json b/bounces/5.4.1.json new file mode 100644 index 0000000..dd374d9 --- /dev/null +++ b/bounces/5.4.1.json @@ -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 ; 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": " <6061d865685c1bb406c127f32451d22d@bayarea-cc.com>" + }, + { + "name": "Message-ID", + "value": "" + }, + { + "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" + ] + } + } + } +} \ No newline at end of file diff --git a/bounces/ooo1.json b/bounces/ooo1.json new file mode 100644 index 0000000..b048c04 --- /dev/null +++ b/bounces/ooo1.json @@ -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 ; 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 " + }, + { + "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 " + ], + "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" + ] + } + } + } +} \ No newline at end of file diff --git a/bounces/ooo2.json b/bounces/ooo2.json new file mode 100644 index 0000000..d34afe0 --- /dev/null +++ b/bounces/ooo2.json @@ -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 ; 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" + ] + } + } + } +} \ No newline at end of file diff --git a/bounces/paypal.json b/bounces/paypal.json new file mode 100644 index 0000000..b85ba2d --- /dev/null +++ b/bounces/paypal.json @@ -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 ; 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": " <6061d865685c1bb406c127f32451d22d@bayarea-cc.com>" + }, + { + "name": "Message-ID", + "value": "" + }, + { + "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" + ] + } + } + } +} \ No newline at end of file diff --git a/lambda_function_outbound.py b/lambda_function_outbound.py index bdbbe1d..e3faf4f 100644 --- a/lambda_function_outbound.py +++ b/lambda_function_outbound.py @@ -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 = message.get('eventType') + + if event_type == 'Bounce': + 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}") - # Event-Type aus dem Event extrahieren - event_type = detail.get('eventType') - - if event_type == 'Send': - source = mail.get('source') - destinations = mail.get('destination', []) + 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 + + 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': msg_id, - 'source': source, - 'destinations': destinations, - 'timestamp': mail.get('timestamp') + '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"Stored SEND event for {msg_id}") - return + + print(f"✓ Stored bounce info for feedbackId {feedback_id}") + + except Exception as e: + print(f"Error handling bounce: {e}") + import traceback + traceback.print_exc() - 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 - table.update_item( - Key={'MessageId': msg_id}, - UpdateExpression="ADD bouncedRecipients :b", - ExpressionAttributeValues={ - ':b': set(bounced) - } - ) - 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 \ No newline at end of file +def handle_send(message): + """ + DEPRECATED - Wird nicht mehr benötigt + Send Events werden jetzt ignoriert + """ + pass \ No newline at end of file diff --git a/test_extract_v2.py b/test_extract_v2.py new file mode 100755 index 0000000..4cfb49f --- /dev/null +++ b/test_extract_v2.py @@ -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 ") + sys.exit(1) \ No newline at end of file diff --git a/worker.py b/worker.py index 550d008..f0e4995 100755 --- a/worker.py +++ b/worker.py @@ -51,145 +51,99 @@ 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() - - # 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') + if not item: + log(f"⚠ No bounce record found for Message-ID: {message_id}") + return None - # 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') - - return None + 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: - # Lookup in DynamoDB - result = msg_table.get_item(Key={'MessageId': original_msg_id}) - item = result.get('Item') + log(f" Looking up Message-ID: {message_id}") + + # Lookup in DynamoDB + bounce_info = get_bounce_info_from_dynamodb(message_id) + + if not bounce_info: + return parsed, False + + # Bounce Info ausgeben + original_source = bounce_info['original_source'] + bounced_recipients = bounce_info['bouncedRecipients'] + bounce_type = bounce_info['bounceType'] + bounce_subtype = bounce_info['bounceSubType'] + + 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] - if not item: - log(f"⚠ No DynamoDB record found for {original_msg_id}") - return parsed, False - - # Treffer! - orig_source = item.get('source', '') - orig_destinations = item.get('destinations', []) - original_recipient = orig_destinations[0] if orig_destinations else '' - - if original_recipient: - log(f"✓ Found original sender: {orig_source} -> intended for {original_recipient}") - - # Rewrite Headers - parsed['X-Original-SES-From'] = parsed.get('From', '') - parsed.replace_header('From', original_recipient) - - if not parsed.get('Reply-To'): - parsed['Reply-To'] = original_recipient - - if 'delivery status notification' in subject.lower(): - parsed.replace_header('Subject', f"Delivery Status: {original_recipient}") - - return parsed, True - - except Exception as e: - log(f"⚠ DynamoDB Error: {e}") + # Rewrite Headers + parsed['X-Original-SES-From'] = parsed.get('From', '') + parsed['X-Bounce-Type'] = f"{bounce_type}/{bounce_subtype}" + parsed.replace_header('From', new_from) + + if not parsed.get('Reply-To'): + parsed['Reply-To'] = new_from + + # 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 + log("⚠ No bounced recipients found in bounce info") return parsed, False - + def signal_handler(signum, frame): global shutdown_requested print(f"\n⚠ Shutdown signal received (signal {signum})")