From bac5aebbb4382273f717281a0162b5dd18625650 Mon Sep 17 00:00:00 2001 From: Andreas Knuth Date: Sun, 16 Nov 2025 17:04:25 -0600 Subject: [PATCH] aktueller Umbaustatus --- DMS/docker-compose.yml | 126 ++++++++++++ basic_setup/awsiam.sh | 127 ++++++++++++ basic_setup/awss3.sh | 85 ++++++++ basic_setup/awsses.sh | 155 +++++++++++++++ basic_setup/cloudflareDns.sh | 160 +++++++++++++++ basic_setup/setup_email_domain.sh | 38 ++++ lambda_function.py | 317 +++++++++++++----------------- lambda_function_outbound.py | 30 +-- setupSNSEventAndDynamo.sh | 46 +++++ 9 files changed, 886 insertions(+), 198 deletions(-) create mode 100644 DMS/docker-compose.yml create mode 100755 basic_setup/awsiam.sh create mode 100755 basic_setup/awss3.sh create mode 100755 basic_setup/awsses.sh create mode 100755 basic_setup/cloudflareDns.sh create mode 100755 basic_setup/setup_email_domain.sh create mode 100644 setupSNSEventAndDynamo.sh diff --git a/DMS/docker-compose.yml b/DMS/docker-compose.yml new file mode 100644 index 0000000..f8a3996 --- /dev/null +++ b/DMS/docker-compose.yml @@ -0,0 +1,126 @@ +services: + + mailserver: + image: docker.io/mailserver/docker-mailserver:latest + container_name: mailserver-new + hostname: mail.email-srvr.com + domainname: email-srvr.com + ports: + - "25:25" # SMTP (parallel zu MailCow auf Port 25) + - "587:587" # SMTP Submission + - "465:465" # SMTP SSL + - "143:143" # IMAP + - "993:993" # IMAP SSL + - "110:110" # POP3 + - "995:995" # POP3 SSL + volumes: + - ./docker-data/dms/mail-data/:/var/mail/ + - ./docker-data/dms/mail-state/:/var/mail-state/ + - ./docker-data/dms/mail-logs/:/var/log/mail/ + - ./docker-data/dms/config/:/tmp/docker-mailserver/ + # - ./docker-data/dms/config/dovecot/10-master.conf:/etc/dovecot/conf.d/10-master.conf + - /etc/localtime:/etc/localtime:ro + environment: + # Spam & Virus + - ENABLE_SPAMASSASSIN=1 + - SPAMASSASSIN_SPAM_TO_INBOX=1 + - SA_TAG=2.0 + - SA_TAG2=6.0 + - SA_KILL=10.0 + - SA_SPAM_SUBJECT=***SPAM*** + - ENABLE_CLAMAV=1 + - CLAMAV_MESSAGE_SIZE_LIMIT=25M + - ENABLE_AMAVIS=1 + + # DKIM + - ENABLE_OPENDKIM=1 + + # SRS + - ENABLE_SRS=0 + # - SRS_SENDER_CLASSES=envelope_sender + # - SRS_SECRET=EBk/ndWRA2s8ZMQFIXq0mJnS6SRbgoj77wv00PZNpNw= + + # Sieve & POP3 + - ENABLE_MANAGESIEVE=1 + - ENABLE_POP3=1 + + # Security + - ENABLE_FAIL2BAN=0 + - SPOOF_PROTECTION=0 + + # System + - PERMIT_DOCKER=network + + # Amazon SES SMTP Relay + - RELAY_HOST=email-smtp.us-east-2.amazonaws.com + - RELAY_PORT=587 + - RELAY_USER=${SES_SMTP_USER} + - RELAY_PASSWORD=${SES_SMTP_PASSWORD} + + # SSL + - SSL_TYPE=manual + - SSL_CERT_PATH=/tmp/docker-mailserver/ssl/cert.pem + - SSL_KEY_PATH=/tmp/docker-mailserver/ssl/key.pem + + # Postfix + - POSTFIX_OVERRIDE_HOSTNAME=email-srvr.com + - POSTFIX_MYNETWORKS=172.16.0.0/12 172.17.0.0/12 172.18.0.0/12 [::1]/128 [fe80::]/64 + - POSTFIX_MAILBOX_SIZE_LIMIT=0 + - POSTFIX_MESSAGE_SIZE_LIMIT=0 + + # Logging + - LOG_LEVEL=info + cap_add: + - NET_ADMIN + - SYS_PTRACE + restart: unless-stopped + networks: + mail_network: + aliases: + - mail.email-srvr.com + - mailserver + + roundcube: + image: roundcube/roundcubemail:latest + container_name: roundcube-new + depends_on: + - roundcube-db + - mailserver + environment: + - ROUNDCUBEMAIL_DB_TYPE=pgsql + - ROUNDCUBEMAIL_DB_HOST=roundcube-db + - ROUNDCUBEMAIL_DB_NAME=roundcube + - ROUNDCUBEMAIL_DB_USER=roundcube + - ROUNDCUBEMAIL_DB_PASSWORD=${ROUNDCUBE_DB_PASSWORD} + # Einfache Konfiguration ohne SSL-Probleme (für ersten Test) + - ROUNDCUBEMAIL_DEFAULT_HOST=ssl://mail.email-srvr.com + - ROUNDCUBEMAIL_DEFAULT_PORT=993 + - ROUNDCUBEMAIL_SMTP_SERVER=tls://mail.email-srvr.com + - ROUNDCUBEMAIL_SMTP_PORT=587 + - ROUNDCUBEMAIL_PLUGINS=password,managesieve + volumes: + - ./docker-data/roundcube/config:/var/roundcube/config + # ENTFERNEN Sie diese Zeile: + # - ./roundcube-config.php:/var/www/html/config/config.inc.php:ro + networks: + - mail_network + restart: unless-stopped + + roundcube-db: + image: postgres:15 + container_name: roundcube-db-new + environment: + - POSTGRES_DB=roundcube + - POSTGRES_USER=roundcube + - POSTGRES_PASSWORD=${ROUNDCUBE_DB_PASSWORD} + ports: + - "5555:5432" + volumes: + - ./docker-data/roundcube/db:/var/lib/postgresql/data + networks: + - mail_network + restart: unless-stopped + +networks: + mail_network: + external: true \ No newline at end of file diff --git a/basic_setup/awsiam.sh b/basic_setup/awsiam.sh new file mode 100755 index 0000000..aad1440 --- /dev/null +++ b/basic_setup/awsiam.sh @@ -0,0 +1,127 @@ +#!/bin/bash +# awsiam.sh - Erstellt einen IAM-Benutzer für Amazon SES mit SMTP-Zugangsdaten + +# Überprüfen, ob die Domain-Variable gesetzt ist +if [ -z "$DOMAIN_NAME" ]; then + echo "Fehler: DOMAIN_NAME ist nicht gesetzt." + echo "Bitte setzen Sie die Variable mit: export DOMAIN_NAME='IhreDomain.de'" + exit 1 +fi + +# Konfiguration +AWS_REGION=${AWS_REGION:-"us-east-2"} +USER_NAME="${DOMAIN_NAME//./-}-ses-user" # Ersetzt Punkte durch Bindestriche für validen IAM-Username +NODE_SCRIPT_PATH="./generate_ses_smtp_password.js" +OUTPUT_FILE="${DOMAIN_NAME//./_}_ses_credentials.txt" # Sichere Dateibenennung + +# Prüfen, ob das Node.js-Script existiert +if [ ! -f "$NODE_SCRIPT_PATH" ]; then + echo "Fehler: Das Node.js-Script '$NODE_SCRIPT_PATH' wurde nicht gefunden." + echo "Bitte stelle sicher, dass das Script im angegebenen Pfad existiert." + exit 1 +fi + +echo "=== IAM-Benutzer für SES SMTP-Zugang erstellen ===" +echo "Domain: $DOMAIN_NAME" +echo "Region: $AWS_REGION" +echo "IAM-Benutzername: $USER_NAME" + +# -------------------------- +# IAM-User erstellen +# -------------------------- +echo "Erstelle IAM-User: $USER_NAME" +aws iam create-user --user-name $USER_NAME + +# Benutzerdefinierte Policy für SES-Sendeberechtigungen erstellen +POLICY_NAME="${USER_NAME}-SendRawEmailPolicy" +POLICY_DOCUMENT='{ + "Version": "2012-10-17", + "Statement": [ + { + "Effect": "Allow", + "Action": "ses:SendRawEmail", + "Resource": "*" + } + ] +}' + +echo "Erstelle benutzerdefinierte Policy für SES SendRawEmail" +POLICY_ARN=$(aws iam create-policy \ + --policy-name $POLICY_NAME \ + --policy-document "$POLICY_DOCUMENT" \ + --query 'Policy.Arn' \ + --output text) + +echo "Hänge Policy an: $POLICY_ARN" +aws iam attach-user-policy \ + --user-name $USER_NAME \ + --policy-arn $POLICY_ARN + +# Access Key und Secret Key für den User erstellen +echo "Erstelle Access Key für den User: $USER_NAME" +KEY_OUTPUT=$(aws iam create-access-key --user-name $USER_NAME) + +# Keys ausgeben und in Variablen speichern +echo "Zugriffsschlüssel wurden erstellt. Bitte sicher aufbewahren:" +echo "$KEY_OUTPUT" | jq . + +ACCESS_KEY=$(echo "$KEY_OUTPUT" | jq -r .AccessKey.AccessKeyId) +SECRET_KEY=$(echo "$KEY_OUTPUT" | jq -r .AccessKey.SecretAccessKey) + +echo "ACCESS_KEY: $ACCESS_KEY" +echo "SECRET_KEY: $SECRET_KEY" + +echo "WICHTIG: Speichere den Secret Key jetzt, da er später nicht mehr abgerufen werden kann!" + +# -------------------------- +# SMTP Passwort generieren +# -------------------------- +echo -e "\nGeneriere SMTP-Passwort für Region $AWS_REGION..." + +# Führe das Node.js-Script aus, um das SMTP-Passwort zu generieren +SMTP_PASSWORD=$(node "$NODE_SCRIPT_PATH" "$SECRET_KEY" "$AWS_REGION") + +# Prüfen, ob die Ausführung erfolgreich war +if [ $? -ne 0 ]; then + echo "Fehler bei der Generierung des SMTP-Passworts. Bitte überprüfe das Node.js-Script." + exit 1 +fi + +# SMTP-Benutzername ist der Access Key +SMTP_USERNAME="$ACCESS_KEY" + +# Ausgabe der SMTP-Anmeldeinformationen +echo -e "\nSMTP-Anmeldeinformationen für Amazon SES in Region $AWS_REGION:" +echo "--------------------------------------------------------------" +echo "SMTP-Server: email-smtp.$AWS_REGION.amazonaws.com" +echo "SMTP-Port: 587 (TLS) oder 465 (SSL)" +echo "SMTP-Benutzername: $SMTP_USERNAME" +echo "SMTP-Passwort: $SMTP_PASSWORD" + +# Speichere die Anmeldeinformationen in einer Datei +echo -e "\nSpeichere SMTP-Anmeldeinformationen in $OUTPUT_FILE" +cat > "$OUTPUT_FILE" << EOF +DOMAIN_NAME: $DOMAIN_NAME +SMTP-Server: email-smtp.$AWS_REGION.amazonaws.com +SMTP-Port: 587 (TLS) oder 465 (SSL) +SMTP-Benutzername: $SMTP_USERNAME +SMTP-Passwort: $SMTP_PASSWORD + +IAM-Benutzer: $USER_NAME +Access Key ID: $ACCESS_KEY +Secret Access Key: $SECRET_KEY +EOF + +chmod 600 "$OUTPUT_FILE" # Nur für den Besitzer lesbar machen + +# Format für .env-Datei +echo -e "\nFür .env-Datei:" +echo "AWS_SES_SMTP_USERNAME=$SMTP_USERNAME" +echo "AWS_SES_SMTP_PASSWORD=$SMTP_PASSWORD" +echo "AWS_SES_SMTP_HOST=email-smtp.$AWS_REGION.amazonaws.com" +echo "AWS_SES_SMTP_PORT=587" + +echo -e "\nHinweise:" +echo "1. Die SMTP-Anmeldeinformationen wurden in $OUTPUT_FILE gespeichert." +echo "2. Verwenden Sie diese SMTP-Anmeldeinformationen in Ihrer E-Mail-Anwendung oder Ihrem E-Mail-Server." +echo "3. Der IAM-Benutzer hat nur die Berechtigung, E-Mails über SES zu senden." \ No newline at end of file diff --git a/basic_setup/awss3.sh b/basic_setup/awss3.sh new file mode 100755 index 0000000..f5c54e0 --- /dev/null +++ b/basic_setup/awss3.sh @@ -0,0 +1,85 @@ +#!/bin/bash +# awss3.sh - Erstellt einen S3-Bucket für Amazon SES E-Mail-Speicherung + +# Überprüfen, ob die Domain-Variable gesetzt ist +if [ -z "$DOMAIN_NAME" ]; then + echo "Fehler: DOMAIN_NAME ist nicht gesetzt." + echo "Bitte setzen Sie die Variable mit: export DOMAIN_NAME='IhreDomain.de'" + exit 1 +fi + +# Konfiguration +AWS_REGION=${AWS_REGION:-"us-east-2"} +EMAIL_PREFIX=${EMAIL_PREFIX:-"emails/"} +S3_BUCKET_NAME=$(echo "$DOMAIN_NAME" | tr '.' '-' | awk '{print $0 "-emails"}') + +echo "=== S3 Bucket Configuration für $DOMAIN_NAME ===" +echo "Region: $AWS_REGION" +echo "Bucket-Name: $S3_BUCKET_NAME" +echo "E-Mail-Präfix: $EMAIL_PREFIX" + +# ------------------------ +# S3 Bucket erstellen +# ------------------------ +echo "S3 Bucket erstellen..." +aws s3api create-bucket \ + --bucket ${S3_BUCKET_NAME} \ + --region ${AWS_REGION} \ + --create-bucket-configuration LocationConstraint=${AWS_REGION} + +# Öffentlichen Zugriff blockieren +echo "Öffentlichen Zugriff blockieren..." +aws s3api put-public-access-block \ + --bucket ${S3_BUCKET_NAME} \ + --public-access-block-configuration "BlockPublicAcls=true,IgnorePublicAcls=true,BlockPublicPolicy=true,RestrictPublicBuckets=true" + +# Lebenszyklus-Konfiguration hinzufügen +echo "Lebenszyklus-Konfiguration hinzufügen (E-Mails werden nach 90 Tagen gelöscht)..." +aws s3api put-bucket-lifecycle-configuration \ + --bucket ${S3_BUCKET_NAME} \ + --lifecycle-configuration '{ + "Rules": [ + { + "ID": "DeleteOldEmails", + "Status": "Enabled", + "Expiration": { + "Days": 90 + }, + "Filter": { + "Prefix": "" + } + } + ] + }' + +echo "S3 Bucket Policy hinzufügen für SES-Zugriff..." +aws s3api put-bucket-policy \ + --bucket ${S3_BUCKET_NAME} \ + --policy '{ + "Version": "2012-10-17", + "Statement": [ + { + "Effect": "Allow", + "Principal": { + "Service": "ses.amazonaws.com" + }, + "Action": [ + "s3:PutObject", + "s3:GetBucketLocation", + "s3:ListBucket" + ], + "Resource": [ + "arn:aws:s3:::'${S3_BUCKET_NAME}'", + "arn:aws:s3:::'${S3_BUCKET_NAME}'/*" + ] + } + ] + }' + +echo "S3 Bucket $S3_BUCKET_NAME wurde erfolgreich erstellt und konfiguriert." +echo "Bucket-ARN: arn:aws:s3:::$S3_BUCKET_NAME" + +# Exportiere Variablen für andere Scripte +echo +echo "Für die Verwendung in den anderen Scripten setzen Sie:" +echo "export S3_BUCKET_NAME=$S3_BUCKET_NAME" \ No newline at end of file diff --git a/basic_setup/awsses.sh b/basic_setup/awsses.sh new file mode 100755 index 0000000..7b9c4a8 --- /dev/null +++ b/basic_setup/awsses.sh @@ -0,0 +1,155 @@ +#!/bin/bash +# awsses.sh - Konfiguriert Amazon SES für eine Domain und erstellt eine Receipt Rule + +# Überprüfen, ob die Domain-Variable gesetzt ist +if [ -z "$DOMAIN_NAME" ]; then + echo "Fehler: DOMAIN_NAME ist nicht gesetzt." + echo "Bitte setzen Sie die Variable mit: export DOMAIN_NAME='IhreDomain.de'" + exit 1 +fi + +# Überprüfen, ob S3_BUCKET_NAME gesetzt ist +if [ -z "$S3_BUCKET_NAME" ]; then + echo "Warnung: S3_BUCKET_NAME ist nicht gesetzt." + echo "Wird automatisch aus DOMAIN_NAME generiert, verwenden Sie idealerweise zuerst awss3.sh." + S3_BUCKET_NAME=$(echo "$DOMAIN_NAME" | tr '.' '-' | awk '{print $0 "-emails"}') + echo "Generierter Bucket-Name: $S3_BUCKET_NAME" +fi + +# Konfiguration +AWS_REGION=${AWS_REGION:-"us-east-2"} +EMAIL_PREFIX=${EMAIL_PREFIX:-""} +RULE_NAME="store-$(echo "$DOMAIN_NAME" | tr '.' '-')-to-s3" + +echo "=== SES Konfiguration für $DOMAIN_NAME ===" +echo "Region: $AWS_REGION" +echo "S3 Bucket: $S3_BUCKET_NAME" +echo "Receipt Rule Name: $RULE_NAME" + +# ------------------------ +# SES Domain-Identität erstellen +# ------------------------ +echo "SES Domain-Identität erstellen..." +IDENTITY_RESULT=$(aws sesv2 create-email-identity \ + --email-identity ${DOMAIN_NAME} \ + --region ${AWS_REGION}) + +echo "Identity erstellt. Überprüfen Sie die DNS-Einträge für die Domain-Verifizierung." +echo "$IDENTITY_RESULT" | jq . + +# DKIM-Signierung aktivieren +echo "DKIM-Signierung aktivieren..." +aws sesv2 put-email-identity-dkim-attributes \ + --email-identity ${DOMAIN_NAME} \ + --signing-enabled \ + --region ${AWS_REGION} + +# Mail-From-Domain konfigurieren +echo "Mail-From-Domain konfigurieren..." +aws sesv2 put-email-identity-mail-from-attributes \ + --email-identity ${DOMAIN_NAME} \ + --mail-from-domain "mail.${DOMAIN_NAME}" \ + --behavior-on-mx-failure USE_DEFAULT_VALUE \ + --region ${AWS_REGION} + +# Überprüfen, ob der Rule Set existiert, sonst erstellen +echo "Überprüfe oder erstelle Receipt Rule Set..." +RULESET_EXISTS=$(aws ses describe-receipt-rule-sets --region ${AWS_REGION} | jq -r '.RuleSets[] | select(.Name == "bizmatch-ruleset") | .Name') + +if [ -z "$RULESET_EXISTS" ]; then + echo "Receipt Rule Set 'bizmatch-ruleset' existiert nicht, wird erstellt..." + aws ses create-receipt-rule-set --rule-set-name "bizmatch-ruleset" --region ${AWS_REGION} +else + echo "Receipt Rule Set 'bizmatch-ruleset' existiert bereits." +fi + +# Receipt Rule erstellen +echo "Receipt Rule für E-Mail-Empfang erstellen..." +aws ses create-receipt-rule --rule-set-name "bizmatch-ruleset" --rule '{ + "Name": "'"${RULE_NAME}"'", + "Enabled": true, + "ScanEnabled": true, + "Actions": [{ + "S3Action": { + "BucketName": "'"${S3_BUCKET_NAME}"'", + "ObjectKeyPrefix": "'"${EMAIL_PREFIX}"'" + } + }], + "TlsPolicy": "Require", + "Recipients": ["'"${DOMAIN_NAME}"'"] +}' --region ${AWS_REGION} + +# Prüfen, ob der Rule Set aktiv ist +ACTIVE_RULESET=$(aws ses describe-active-receipt-rule-set --region ${AWS_REGION} | jq -r '.Metadata.Name') + +if [ "$ACTIVE_RULESET" != "bizmatch-ruleset" ]; then + echo "Aktiviere Rule Set 'bizmatch-ruleset'..." + aws ses set-active-receipt-rule-set --rule-set-name "bizmatch-ruleset" --region ${AWS_REGION} +else + echo "Rule Set 'bizmatch-ruleset' ist bereits aktiv." +fi + +# ------------------------ +# Lambda-Funktion mit SES verknüpfen +# ------------------------ +echo "Verknüpfe Lambda-Funktion 'ses-to-sqs' mit SES..." + +# Lambda ARN ermitteln +LAMBDA_ARN=$(aws lambda get-function \ + --function-name ses-to-sqs \ + --region ${AWS_REGION} \ + --query 'Configuration.FunctionArn' \ + --output text) + +if [ -z "$LAMBDA_ARN" ]; then + echo "FEHLER: Lambda-Funktion 'ses-to-sqs' nicht gefunden!" + echo "Bitte zuerst Lambda-Funktion deployen." + exit 1 +fi + +echo "Lambda ARN: $LAMBDA_ARN" + +# SES Permission für Lambda hinzufügen (falls noch nicht vorhanden) +echo "Füge SES-Berechtigung zur Lambda-Funktion hinzu..." +aws lambda add-permission \ + --function-name ses-to-sqs \ + --statement-id "AllowSESInvoke-${DOMAIN_NAME//./}" \ + --action "lambda:InvokeFunction" \ + --principal ses.amazonaws.com \ + --source-account $(aws sts get-caller-identity --query Account --output text) \ + --region ${AWS_REGION} 2>/dev/null || echo "Permission bereits vorhanden" + +# Receipt Rule UPDATE: Lambda Action hinzufügen +echo "Aktualisiere Receipt Rule mit Lambda Action..." +aws ses update-receipt-rule --rule-set-name "bizmatch-ruleset" --rule '{ + "Name": "'"${RULE_NAME}"'", + "Enabled": true, + "ScanEnabled": true, + "Actions": [ + { + "S3Action": { + "BucketName": "'"${S3_BUCKET_NAME}"'", + "ObjectKeyPrefix": "'"${EMAIL_PREFIX}"'" + } + }, + { + "LambdaAction": { + "FunctionArn": "'"${LAMBDA_ARN}"'", + "InvocationType": "Event" + } + } + ], + "TlsPolicy": "Require", + "Recipients": ["'"${DOMAIN_NAME}"'"] +}' --region ${AWS_REGION} + +echo "✅ Lambda-Funktion erfolgreich mit SES verknüpft!" + +echo "SES-Konfiguration für $DOMAIN_NAME abgeschlossen." +echo +echo "WICHTIG: Überprüfen Sie die Ausgabe oben für DNS-Einträge, die Sie bei Ihrem DNS-Provider setzen müssen:" +echo "1. DKIM-Einträge (3 CNAME-Einträge)" +echo "2. MAIL FROM MX und TXT-Einträge" +echo "3. SPF-Eintrag (TXT): v=spf1 include:amazonses.com ~all" +echo +echo "Nach dem Setzen der DNS-Einträge kann es bis zu 72 Stunden dauern, bis die Verifizierung abgeschlossen ist." \ No newline at end of file diff --git a/basic_setup/cloudflareDns.sh b/basic_setup/cloudflareDns.sh new file mode 100755 index 0000000..1e84649 --- /dev/null +++ b/basic_setup/cloudflareDns.sh @@ -0,0 +1,160 @@ +#!/bin/bash + +# Cloudflare API-Konfiguration +# Setze deine API-Schlüssel und Zone-ID als Umgebungsvariablen oder ersetze sie direkt + +# CF_ZONE_ID="1b7756cee93ed8ba8c05bdc3cb0a5da8" # Die Zone-ID deiner Domain bei Cloudflare +AWS_REGION="us-east-2" # AWS-Region +if [ -z "$DOMAIN_NAME" ]; then + echo "Fehler: DOMAIN_NAME ist nicht gesetzt." + echo "Bitte setzen Sie die Variable mit: export DOMAIN_NAME='IhreDomain.de'" + exit 1 # Skript mit Fehlercode beenden +fi +# Überprüfen, ob der erforderliche API-Token gesetzt ist +if [ -z "$CF_API_TOKEN" ]; then + echo "Fehler: Bitte setze CF_API_TOKEN als Umgebungsvariable oder im Skript." + exit 1 +fi + +# Zone ID basierend auf Domain-Namen abrufen +echo "Zone ID für $DOMAIN_NAME abrufen..." +ZONE_RESPONSE=$(curl -s -X GET "https://api.cloudflare.com/client/v4/zones?name=$DOMAIN_NAME" \ + -H "Authorization: Bearer $CF_API_TOKEN" \ + -H "Content-Type: application/json") + +# Überprüfen, ob die Antwort erfolgreich war +if [ "$(echo $ZONE_RESPONSE | jq -r '.success')" != "true" ]; then + echo "Fehler beim Abrufen der Zone ID:" + echo $ZONE_RESPONSE | jq . + exit 1 +fi + +# Zone ID extrahieren +CF_ZONE_ID=$(echo $ZONE_RESPONSE | jq -r '.result[0].id') + +# Überprüfen, ob eine Zone ID gefunden wurde +if [ -z "$CF_ZONE_ID" ] || [ "$CF_ZONE_ID" = "null" ]; then + echo "Keine Zone ID für $DOMAIN_NAME gefunden. Bitte stelle sicher, dass die Domain bei Cloudflare registriert ist." + exit 1 +fi + +echo "Zone ID für $DOMAIN_NAME: $CF_ZONE_ID" + +# Hilfsfunktion für DNS-Einträge anlegen +create_dns_record() { + local TYPE=$1 + local NAME=$2 + local CONTENT=$3 + local PROXIED=$4 + local TTL=$5 + local PRIORITY=$6 # Neu: MX-Priority + + # Standardwerte für Proxied und TTL setzen, falls nicht angegeben + if [ -z "$PROXIED" ]; then + PROXIED="false" + fi + + if [ -z "$TTL" ]; then + TTL=3600 # 1 Stunde + fi + + echo "Erstelle $TYPE-Eintrag für $NAME mit Inhalt $CONTENT..." + + # Json Payload vorbereiten abhängig vom Record-Typ + local JSON_DATA="" + + if [ "$TYPE" = "MX" ]; then + # Bei MX-Einträgen müssen wir die Priority separat angeben + if [ -z "$PRIORITY" ]; then + PRIORITY=10 # Standard-Priority, falls nicht angegeben + fi + + JSON_DATA="{ + \"type\": \"$TYPE\", + \"name\": \"$NAME\", + \"content\": \"$CONTENT\", + \"ttl\": $TTL, + \"priority\": $PRIORITY, + \"proxied\": $PROXIED + }" + elif [ "$TYPE" = "TXT" ]; then + # Bei TXT-Einträgen müssen wir sicherstellen, dass der Inhalt in Anführungszeichen steht + # Aber Anführungszeichen innerhalb von JSON müssen escaped werden + # Wir entfernen zuerst alle vorhandenen Anführungszeichen und fügen sie dann korrekt hinzu + CONTENT=$(echo "$CONTENT" | sed 's/"//g') + + JSON_DATA="{ + \"type\": \"$TYPE\", + \"name\": \"$NAME\", + \"content\": \"\\\"$CONTENT\\\"\", + \"ttl\": $TTL, + \"proxied\": $PROXIED + }" + else + # Für alle anderen Record-Typen (z.B. CNAME) + JSON_DATA="{ + \"type\": \"$TYPE\", + \"name\": \"$NAME\", + \"content\": \"$CONTENT\", + \"ttl\": $TTL, + \"proxied\": $PROXIED + }" + fi + + # API-Aufruf an Cloudflare + curl -s -X POST "https://api.cloudflare.com/client/v4/zones/$CF_ZONE_ID/dns_records" \ + -H "Authorization: Bearer $CF_API_TOKEN" \ + -H "Content-Type: application/json" \ + --data "$JSON_DATA" | jq . +} + +# DKIM-Einträge abrufen und bei Cloudflare eintragen +echo "DKIM-Tokens abrufen von AWS SES..." +DKIM_TOKENS=$(aws ses get-identity-dkim-attributes \ + --identities ${DOMAIN_NAME} \ + --region ${AWS_REGION} \ + --query "DkimAttributes.\"${DOMAIN_NAME}\".DkimTokens" \ + --output text) + +# Überprüfen, ob DKIM-Tokens abgerufen wurden +if [ -z "$DKIM_TOKENS" ]; then + echo "Fehler: Konnte DKIM-Tokens nicht abrufen. Ist die Domain bei AWS SES verifiziert?" + exit 1 +fi + +# Domain-Verifizierungstoken abrufen +VERIFICATION_TOKEN=$(aws ses get-identity-verification-attributes \ + --identities ${DOMAIN_NAME} \ + --region ${AWS_REGION} \ + --query "VerificationAttributes.\"${DOMAIN_NAME}\".VerificationToken" \ + --output text) + +# DKIM-Einträge anlegen +echo "DKIM-Einträge anlegen bei Cloudflare..." +for TOKEN in ${DKIM_TOKENS}; do + create_dns_record "CNAME" "${TOKEN}._domainkey.${DOMAIN_NAME}" "${TOKEN}.dkim.amazonses.com" "false" 3600 +done + +# Domain-Verifizierungs-TXT-Eintrag anlegen +echo "Domain-Verifizierungs-TXT-Eintrag anlegen bei Cloudflare..." +create_dns_record "TXT" "_amazonses.${DOMAIN_NAME}" "${VERIFICATION_TOKEN}" "false" 3600 + +# MX-Einträge anlegen +echo "MX-Einträge anlegen bei Cloudflare..." +create_dns_record "MX" "${DOMAIN_NAME}" "inbound-smtp.${AWS_REGION}.amazonaws.com" "false" 3600 10 +create_dns_record "MX" "mail.${DOMAIN_NAME}" "feedback-smtp.${AWS_REGION}.amazonses.com" "false" 3600 10 + +# CNAME für mail.{Domain} anlegen +echo "CNAME für mail.${DOMAIN_NAME} anlegen bei Cloudflare..." +create_dns_record "CNAME" "imap.${DOMAIN_NAME}" "${DOMAIN_NAME}" "false" 3600 + +# SPF-Eintrag anlegen +echo "SPF-Eintrag anlegen bei Cloudflare..." +create_dns_record "TXT" "mail.${DOMAIN_NAME}" "v=spf1 include:amazonses.com ~all" "false" 3600 + +# DMARC-Eintrag anlegen +echo "DMARC-Eintrag anlegen bei Cloudflare..." +create_dns_record "TXT" "_dmarc.${DOMAIN_NAME}" "v=DMARC1; p=quarantine; pct=100; rua=mailto:postmaster@${DOMAIN_NAME}" "false" 3600 + +echo "DNS-Einrichtung abgeschlossen." +echo "Es kann bis zu 72 Stunden dauern, bis AWS SES die Domain verifiziert hat." \ No newline at end of file diff --git a/basic_setup/setup_email_domain.sh b/basic_setup/setup_email_domain.sh new file mode 100755 index 0000000..b10905f --- /dev/null +++ b/basic_setup/setup_email_domain.sh @@ -0,0 +1,38 @@ +#!/bin/bash +# setup_email_domain.sh - Ein Wrapper-Script, das alle drei Skripte in der richtigen Reihenfolge ausführt + +# Überprüfen, ob die Domain-Variable gesetzt ist +if [ -z "$1" ]; then + echo "Fehler: Keine Domain angegeben." + echo "Verwendung: ./setup_email_domain.sh domain.de [region]" + exit 1 +fi + +DOMAIN_NAME=$1 +AWS_REGION=${2:-"us-east-2"} + +# Variablen exportieren +export DOMAIN_NAME +export AWS_REGION + +echo "=== AWS E-Mail-Infrastruktur für $DOMAIN_NAME einrichten ===" +echo "AWS-Region: $AWS_REGION" +echo + +# Skripte nacheinander ausführen +echo "1. S3-Bucket erstellen..." +./awss3.sh +echo + +echo "2. SES-Konfiguration einrichten..." +export S3_BUCKET_NAME=$(echo "$DOMAIN_NAME" | tr '.' '-' | awk '{print $0 "-emails"}') +./awsses.sh +echo + +echo "3. IAM-Benutzer und SMTP-Zugangsdaten erstellen..." +./awsiam.sh +echo + +echo "=== Setup abgeschlossen ===" +echo "Alle Schritte wurden abgeschlossen. Bitte überprüfen Sie die Ausgaben der einzelnen Skripte." +echo "Vergessen Sie nicht, die benötigten DNS-Einträge für Ihre Domain zu setzen, um die SES-Verifizierung abzuschließen." \ No newline at end of file diff --git a/lambda_function.py b/lambda_function.py index 484763e..872792e 100644 --- a/lambda_function.py +++ b/lambda_function.py @@ -19,13 +19,48 @@ msg_table = dynamo.Table('ses-outbound-messages') PROCESSED_KEY = 'processed' PROCESSED_VALUE = 'true' -def is_ses_autoresponse(parsed): +def is_ses_bounce_or_autoreply(parsed): + """Erkennt SES Bounces und Auto-Replies""" 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 - ) + + # SES MAILER-DAEMON oder Auto-Submitted Header + 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 + + +def extract_original_message_id(parsed): + """Extrahiert die ursprüngliche Message-ID aus In-Reply-To oder References""" + # Versuche In-Reply-To + in_reply_to = (parsed.get('In-Reply-To') or '').strip() + if in_reply_to: + msg_id = in_reply_to + if msg_id.startswith('<') and '>' in msg_id: + msg_id = msg_id[1:msg_id.find('>')] + + # ✅ WICHTIG: Entferne @amazonses.com Suffix falls vorhanden + if '@' in msg_id: + msg_id = msg_id.split('@')[0] + + return msg_id + + # Fallback: References Header (nimm die ERSTE ID) + refs = (parsed.get('References') or '').strip() + if refs: + first_ref = refs.split()[0] + if first_ref.startswith('<') and '>' in first_ref: + first_ref = first_ref[1:first_ref.find('>')] + + # ✅ WICHTIG: Entferne @amazonses.com Suffix falls vorhanden + if '@' in first_ref: + first_ref = first_ref.split('@')[0] + + return first_ref + + return None + def domain_to_bucket(domain: str) -> str: """Konvertiert Domain zu S3 Bucket Namen""" @@ -159,7 +194,7 @@ def send_to_queue(queue_url: str, bucket: str, key: str, 'bucket': bucket, 'key': key, 'from': from_addr, - 'recipients': recipients, # Liste aller Empfänger + 'recipients': recipients, 'domain': domain, 'subject': subject, 'message_id': message_id, @@ -194,7 +229,6 @@ def send_to_queue(queue_url: str, bucket: str, key: str, print(f"✓ Queued to {queue_name}: SQS MessageId={sqs_message_id}") print(f" Recipients: {len(recipients)} - {', '.join(recipients)}") - # Als queued markieren mark_as_queued(bucket, key, queue_name) return sqs_message_id @@ -206,8 +240,7 @@ def send_to_queue(queue_url: str, bucket: str, key: str, def lambda_handler(event, context): """ - Lambda Handler für SES Events - Eine Domain pro Event = eine Queue Message mit allen Recipients + Lambda Handler für SES Inbound Events """ print(f"{'='*70}") @@ -221,10 +254,7 @@ def lambda_handler(event, context): ses = record['ses'] except (KeyError, IndexError) as e: print(f"✗ Invalid event structure: {e}") - return { - 'statusCode': 400, - 'body': json.dumps({'error': 'Invalid SES event'}) - } + return {'statusCode': 400, 'body': json.dumps({'error': 'Invalid SES event'})} mail = ses['mail'] receipt = ses['receipt'] @@ -234,21 +264,14 @@ def lambda_handler(event, context): timestamp = mail.get('timestamp', '') recipients = receipt.get('recipients', []) - # FRÜHES LOGGING: S3 Key und Recipients print(f"\n🔑 S3 Key: {message_id}") print(f"👥 Recipients ({len(recipients)}): {', '.join(recipients)}") if not recipients: print(f"✗ No recipients found in event") - return { - 'statusCode': 400, - 'body': json.dumps({ - 'error': 'No recipients in event', - 'message_id': message_id - }) - } + return {'statusCode': 400, 'body': json.dumps({'error': 'No recipients'})} - # Domain extrahieren (alle Recipients haben gleiche Domain!) + # Domain extrahieren domain = recipients[0].split('@')[1].lower() bucket = domain_to_bucket(domain) @@ -257,187 +280,129 @@ def lambda_handler(event, context): print(f" From: {source}") print(f" Domain: {domain}") print(f" Bucket: {bucket}") - print(f" Timestamp: {timestamp}") - print(f" Recipients: {len(recipients)}") - # Queue für Domain ermitteln + # Queue ermitteln try: queue_url = get_queue_url_for_domain(domain) queue_name = queue_url.split('/')[-1] - print(f" Queue: {queue_name}") except Exception as e: - print(f"\n✗ ERROR: {e}") - return { - 'statusCode': 500, - 'body': json.dumps({ - 'error': 'queue_not_configured', - 'domain': domain, - 'recipients': recipients, - 'message': str(e) - }) - } + print(f"\n✗ Queue ERROR: {e}") + return {'statusCode': 500, 'body': json.dumps({'error': str(e)})} # S3 Object finden try: - print(f"\n📦 Searching S3...") - response = s3.list_objects_v2( - Bucket=bucket, - Prefix=message_id, - MaxKeys=1 - ) + response = s3.list_objects_v2(Bucket=bucket, Prefix=message_id, MaxKeys=1) - if 'Contents' not in response or not response['Contents']: - raise Exception(f"No S3 object found for message {message_id}") + if 'Contents' not in response: + raise Exception(f"No S3 object found for {message_id}") key = response['Contents'][0]['Key'] size = response['Contents'][0]['Size'] - print(f" Found: s3://{bucket}/{key}") - print(f" Size: {size:,} bytes ({size/1024:.1f} KB)") + print(f" Found: s3://{bucket}/{key} ({size/1024:.1f} KB)") except Exception as e: print(f"\n✗ S3 ERROR: {e}") - return { - 'statusCode': 404, - 'body': json.dumps({ - 'error': 's3_object_not_found', - 'message_id': message_id, - 'bucket': bucket, - 'details': str(e) - }) - } + return {'statusCode': 404, 'body': json.dumps({'error': str(e)})} # Duplicate Check - print(f"\n🔍 Checking for duplicates...") if is_already_processed(bucket, key): - print(f" Already processed, skipping") - return { - 'statusCode': 200, - 'body': json.dumps({ - 'status': 'already_processed', - 'message_id': message_id, - 'recipients': recipients - }) - } + return {'statusCode': 200, 'body': json.dumps({'status': 'already_processed'})} - # Processing Lock setzen - print(f"\n🔒 Setting processing lock...") + # Processing Lock if not set_processing_lock(bucket, key): - print(f" Already being processed by another instance") - return { - 'statusCode': 200, - 'body': json.dumps({ - 'status': 'already_processing', - 'message_id': message_id, - 'recipients': recipients - }) - } + return {'statusCode': 200, 'body': json.dumps({'status': 'already_processing'})} - # E-Mail laden um Subject zu extrahieren + # E-Mail laden und ggf. umschreiben subject = '(unknown)' - raw_bytes = b'' - parsed = None modified = False - + try: - print(f"\n📖 Reading email for metadata...") + print(f"\n📖 Reading email...") obj = s3.get_object(Bucket=bucket, Key=key) raw_bytes = obj['Body'].read() 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: - print(f"\n📤 Queuing to {queue_name}...") + parsed = BytesParser(policy=SMTPPolicy).parsebytes(raw_bytes) + subject = parsed.get('Subject', '(no subject)') + print(f" Subject: {subject}") + + # 🔁 Auto-Response / Bounce Detection + if is_ses_bounce_or_autoreply(parsed): + print(f" 🔍 Detected auto-response/bounce from SES") + + # Extrahiere ursprüngliche Message-ID + original_msg_id = extract_original_message_id(parsed) + + if original_msg_id: + print(f" 📋 Original MessageId: {original_msg_id}") + + try: + # Hole Original-Send aus DynamoDB + result = msg_table.get_item(Key={'MessageId': original_msg_id}) + original_send = result.get('Item') + + if original_send: + orig_source = original_send.get('source', '') + orig_destinations = original_send.get('destinations', []) + + print(f" ✓ Found original send:") + print(f" Original From: {orig_source}") + print(f" Original To: {orig_destinations}") + + # **WICHTIG**: Der erste Empfänger war der eigentliche Empfänger + original_recipient = orig_destinations[0] if orig_destinations else '' + + if original_recipient: + # Absender umschreiben auf ursprünglichen Empfänger + original_from = parsed.get('From', '') + parsed['X-Original-SES-From'] = original_from + parsed['X-Original-MessageId'] = original_msg_id + + # **From auf den ursprünglichen Empfänger setzen** + parsed.replace_header('From', original_recipient) + + # Reply-To optional beibehalten + if not parsed.get('Reply-To'): + parsed['Reply-To'] = original_recipient + + # Subject anpassen falls nötig + if 'delivery status notification' in subject.lower(): + parsed.replace_header('Subject', f"Delivery Status: {orig_destinations[0]}") + + raw_bytes = parsed.as_bytes() + modified = True + + print(f" ✅ Rewritten: From={original_recipient}") + else: + print(f" ⚠ No DynamoDB record found for {original_msg_id}") + + except Exception as e: + print(f" ⚠ DynamoDB lookup failed: {e}") + else: + print(f" ⚠ Could not extract original Message-ID") + + # S3 aktualisieren falls modified + if modified: + s3.put_object(Bucket=bucket, Key=key, Body=raw_bytes, Metadata=metadata) + print(f" 💾 Updated S3 object with rewritten email") + + except Exception as e: + print(f" ⚠ Email parsing error: {e}") + + # In Queue einreihen + try: sqs_message_id = send_to_queue( queue_url=queue_url, bucket=bucket, key=key, from_addr=source, - recipients=recipients, # ALLE Recipients + recipients=recipients, domain=domain, subject=subject, message_id=message_id ) - print(f"\n{'='*70}") - print(f"✅ SUCCESS - Email queued for delivery") - print(f"{'='*70}\n") + print(f"\n✅ SUCCESS - Queued for delivery\n") return { 'statusCode': 200, @@ -445,26 +410,10 @@ def lambda_handler(event, context): 'status': 'queued', 'message_id': message_id, 'sqs_message_id': sqs_message_id, - 'queue': queue_name, - 'domain': domain, - 'recipients': recipients, - 'recipient_count': len(recipients), - 'subject': subject + 'modified': modified }) } except Exception as e: - print(f"\n{'='*70}") - print(f"✗ FAILED TO QUEUE") - print(f"{'='*70}") - print(f"Error: {e}") - - return { - 'statusCode': 500, - 'body': json.dumps({ - 'error': 'failed_to_queue', - 'message': str(e), - 'message_id': message_id, - 'recipients': recipients - }) - } \ No newline at end of file + print(f"\n✗ QUEUE FAILED: {e}") + return {'statusCode': 500, 'body': json.dumps({'error': str(e)})} \ No newline at end of file diff --git a/lambda_function_outbound.py b/lambda_function_outbound.py index c67cd55..bdbbe1d 100644 --- a/lambda_function_outbound.py +++ b/lambda_function_outbound.py @@ -5,18 +5,20 @@ 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 - + print(f"Received event: {event}") + + detail = event.get('detail', {}) + mail = detail.get('mail', {}) + msg_id = mail.get('messageId') + if not msg_id: print("No MessageId in event") return - - if event_type == 'SEND': - # wie bisher + + # 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( @@ -27,9 +29,10 @@ def lambda_handler(event, context): 'timestamp': mail.get('timestamp') } ) + print(f"Stored SEND event for {msg_id}") return - if event_type == 'BOUNCE': + if event_type == 'Bounce': bounce = detail.get('bounce', {}) bounced = [ r.get('emailAddress') @@ -40,18 +43,17 @@ def lambda_handler(event, context): 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 + ':b': set(bounced) } ) print(f"Updated {msg_id} with bouncedRecipients={bounced}") return - if event_type == 'COMPLAINT': + if event_type == 'Complaint': complaint = detail.get('complaint', {}) complained = [ r.get('emailAddress') @@ -69,4 +71,4 @@ def lambda_handler(event, context): } ) print(f"Updated {msg_id} with complaintRecipients={complained}") - return + return \ No newline at end of file diff --git a/setupSNSEventAndDynamo.sh b/setupSNSEventAndDynamo.sh new file mode 100644 index 0000000..b332f48 --- /dev/null +++ b/setupSNSEventAndDynamo.sh @@ -0,0 +1,46 @@ +#!/bin/bash +# setupSNSEventAndDynamo.sh - Sendet eine E-Mail aus S3 manuell in die SQS Queue + +set -e + +aws sesv2 create-configuration-set \ + --region us-east-2 \ + --configuration-set-name relay-outbound + +aws sesv2 create-configuration-set-event-destination \ + --region us-east-2 \ + --configuration-set-name relay-outbound \ + --event-destination-name relay-outbound-send-events \ + --event-destination '{ + "Enabled": true, + "MatchingEventTypes": ["SEND"], + "EventBridgeDestination": { + "EventBusArn": "arn:aws:events:us-east-2:[ACCOUNT-ID]:event-bus/default" + } + }' + +aws events put-rule \ + --region us-east-2 \ + --name ses-relay-outbound-send \ + --event-pattern '{ + "source": ["aws.ses"], + "detail-type": ["Email Sent", "Email Bounced"] + }' + +aws events put-targets \ + --region us-east-2 \ + --rule ses-relay-outbound-send \ + --targets "Id"="relay-outbound-target","Arn"="arn:aws:lambda:us-east-2:[ACCOUNT-ID]:function:relay-outbound" + +aws sesv2 put-email-identity-configuration-set-attributes \ + --region us-east-2 \ + --email-identity bayarea-cc.com \ + --configuration-set-name relay-outbound + +# Dynamo +aws dynamodb create-table \ + --region us-east-2 \ + --table-name ses-outbound-messages \ + --attribute-definitions AttributeName=MessageId,AttributeType=S \ + --key-schema AttributeName=MessageId,KeyType=HASH \ + --billing-mode PAY_PER_REQUEST \ No newline at end of file