cleanwork
This commit is contained in:
97
lambda/lambda_function_bounce.py
Normal file
97
lambda/lambda_function_bounce.py
Normal file
@@ -0,0 +1,97 @@
|
||||
import json
|
||||
import boto3
|
||||
import os
|
||||
from datetime import datetime
|
||||
|
||||
# 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):
|
||||
"""
|
||||
Verarbeitet SES Events:
|
||||
- Bounce Events: Speichert bounce details in DynamoDB
|
||||
- Send Events: Ignoriert (nicht mehr benötigt)
|
||||
"""
|
||||
|
||||
print(f"Received event: {json.dumps(event)}")
|
||||
|
||||
# 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}")
|
||||
|
||||
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': 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 bounce info for feedbackId {feedback_id}")
|
||||
|
||||
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
|
||||
123
lambda/ses_sns_shim_global.py
Normal file
123
lambda/ses_sns_shim_global.py
Normal file
@@ -0,0 +1,123 @@
|
||||
import json
|
||||
import os
|
||||
import boto3
|
||||
import uuid
|
||||
import logging
|
||||
from datetime import datetime
|
||||
from botocore.exceptions import ClientError
|
||||
import time
|
||||
import random
|
||||
|
||||
# Logging konfigurieren
|
||||
logger = logging.getLogger()
|
||||
logger.setLevel(logging.INFO)
|
||||
|
||||
sqs = boto3.client('sqs')
|
||||
|
||||
# Retry-Konfiguration
|
||||
MAX_RETRIES = 3
|
||||
BASE_BACKOFF = 1 # Sekunden
|
||||
|
||||
def exponential_backoff(attempt):
|
||||
"""Exponential Backoff mit Jitter"""
|
||||
return BASE_BACKOFF * (2 ** attempt) + random.uniform(0, 1)
|
||||
|
||||
def get_queue_url(domain):
|
||||
"""
|
||||
Generiert Queue-Namen aus Domain und holt URL.
|
||||
Konvention: domain.tld -> domain-tld-queue
|
||||
"""
|
||||
queue_name = domain.replace('.', '-') + '-queue'
|
||||
try:
|
||||
response = sqs.get_queue_url(QueueName=queue_name)
|
||||
return response['QueueUrl']
|
||||
except ClientError as e:
|
||||
if e.response['Error']['Code'] == 'AWS.SimpleQueueService.NonExistentQueue':
|
||||
logger.error(f"Queue nicht gefunden für Domain: {domain}")
|
||||
raise ValueError(f"Keine Queue für Domain {domain}")
|
||||
else:
|
||||
raise
|
||||
|
||||
def lambda_handler(event, context):
|
||||
"""
|
||||
Nimmt SES Event entgegen, extrahiert Domain dynamisch,
|
||||
verpackt Metadaten als 'Fake SNS' und sendet an die domain-spezifische SQS.
|
||||
Mit integrierter Retry-Logik für SQS-Send.
|
||||
"""
|
||||
try:
|
||||
records = event.get('Records', [])
|
||||
logger.info(f"Received event with {len(records)} records.")
|
||||
|
||||
for record in records:
|
||||
ses_data = record.get('ses', {})
|
||||
if not ses_data:
|
||||
logger.warning(f"Invalid SES event: Missing 'ses' in record: {record}")
|
||||
continue
|
||||
|
||||
mail = ses_data.get('mail', {})
|
||||
receipt = ses_data.get('receipt', {})
|
||||
|
||||
# Domain extrahieren (aus erstem Recipient)
|
||||
recipients = receipt.get('recipients', []) or mail.get('destination', [])
|
||||
if not recipients:
|
||||
logger.warning("No recipients in event - skipping")
|
||||
continue
|
||||
|
||||
first_recipient = recipients[0]
|
||||
domain = first_recipient.split('@')[-1].lower()
|
||||
if not domain:
|
||||
logger.error("Could not extract domain from recipient")
|
||||
continue
|
||||
|
||||
# Wichtige Metadaten loggen
|
||||
msg_id = mail.get('messageId', 'unknown')
|
||||
source = mail.get('source', 'unknown')
|
||||
logger.info(f"Processing Message-ID: {msg_id} for domain: {domain}")
|
||||
logger.info(f" From: {source}")
|
||||
logger.info(f" To: {recipients}")
|
||||
|
||||
# SES JSON als String serialisieren
|
||||
ses_json_string = json.dumps(ses_data)
|
||||
|
||||
# Payload Größe loggen und checken (Safeguard)
|
||||
payload_size = len(ses_json_string.encode('utf-8'))
|
||||
logger.info(f" Metadata Payload Size: {payload_size} bytes")
|
||||
if payload_size > 200000: # Arbitrary Limit < SQS 256KB
|
||||
raise ValueError("Payload too large for SQS")
|
||||
|
||||
# Fake SNS Payload
|
||||
fake_sns_payload = {
|
||||
"Type": "Notification",
|
||||
"MessageId": str(uuid.uuid4()),
|
||||
"TopicArn": "arn:aws:sns:ses-shim:global-topic",
|
||||
"Subject": "Amazon SES Email Receipt Notification",
|
||||
"Message": ses_json_string,
|
||||
"Timestamp": datetime.utcnow().isoformat() + "Z"
|
||||
}
|
||||
|
||||
# Queue URL dynamisch holen
|
||||
queue_url = get_queue_url(domain)
|
||||
|
||||
# SQS Send mit Retries
|
||||
attempt = 0
|
||||
while attempt < MAX_RETRIES:
|
||||
try:
|
||||
sqs.send_message(
|
||||
QueueUrl=queue_url,
|
||||
MessageBody=json.dumps(fake_sns_payload)
|
||||
)
|
||||
logger.info(f"✅ Successfully forwarded {msg_id} to SQS: {queue_url}")
|
||||
break
|
||||
except ClientError as e:
|
||||
attempt += 1
|
||||
error_code = e.response['Error']['Code']
|
||||
logger.warning(f"Retry {attempt}/{MAX_RETRIES} for SQS send: {error_code} - {str(e)}")
|
||||
if attempt == MAX_RETRIES:
|
||||
raise
|
||||
time.sleep(exponential_backoff(attempt))
|
||||
|
||||
return {'status': 'ok'}
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"❌ Critical Error in Lambda Shim: {str(e)}", exc_info=True)
|
||||
raise e
|
||||
Reference in New Issue
Block a user