Compare commits
2 Commits
df0d92ba27
...
6a3a9264f7
| Author | SHA1 | Date |
|---|---|---|
|
|
6a3a9264f7 | |
|
|
bac5aebbb4 |
|
|
@ -0,0 +1,118 @@
|
||||||
|
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:
|
||||||
|
# Wichtig: Rspamd und andere Services deaktivieren für ersten Test
|
||||||
|
- ENABLE_RSPAMD=0
|
||||||
|
- ENABLE_OPENDKIM=1
|
||||||
|
- ENABLE_OPENDMARC=0
|
||||||
|
- ENABLE_POLICYD_SPF=0
|
||||||
|
- ENABLE_AMAVIS=0
|
||||||
|
- ENABLE_SPAMASSASSIN=0
|
||||||
|
- ENABLE_POSTGREY=0
|
||||||
|
- RSPAMD_GREYLISTING=0
|
||||||
|
- ENABLE_CLAMAV=0
|
||||||
|
#- ENABLE_FAIL2BAN=1
|
||||||
|
- ENABLE_FAIL2BAN=0
|
||||||
|
- ENABLE_MANAGESIEVE=1
|
||||||
|
- ENABLE_POP3=1
|
||||||
|
- RSPAMD_LEARN=1
|
||||||
|
- MOVE_SPAM_TO_JUNK=1
|
||||||
|
- RSPAMD_CHECK_AUTHENTICATED=0
|
||||||
|
- RSPAMD_HFILTER=0
|
||||||
|
- ONE_DIR=1
|
||||||
|
- ENABLE_UPDATE_CHECK=0
|
||||||
|
- PERMIT_DOCKER=network
|
||||||
|
# - PERMIT_DOCKER=empty
|
||||||
|
- SSL_TYPE=manual
|
||||||
|
- SSL_CERT_PATH=/tmp/docker-mailserver/ssl/cert.pem
|
||||||
|
- SSL_KEY_PATH=/tmp/docker-mailserver/ssl/key.pem
|
||||||
|
# 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}
|
||||||
|
# Weitere Einstellungen
|
||||||
|
- 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
|
||||||
|
- SPOOF_PROTECTION=0
|
||||||
|
- ENABLE_SRS=1
|
||||||
|
- SRS_SENDER_CLASSES=envelope_sender,header_sender
|
||||||
|
- SRS_SECRET=EBk/ndWRA2s8ZMQFIXq0mJnS6SRbgoj77wv00PZNpNw=
|
||||||
|
# Debug-Einstellungen
|
||||||
|
- LOG_LEVEL=debug
|
||||||
|
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
|
||||||
|
|
@ -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."
|
||||||
|
|
@ -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"
|
||||||
|
|
@ -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."
|
||||||
|
|
@ -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."
|
||||||
|
|
@ -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."
|
||||||
|
|
@ -19,13 +19,48 @@ msg_table = dynamo.Table('ses-outbound-messages')
|
||||||
PROCESSED_KEY = 'processed'
|
PROCESSED_KEY = 'processed'
|
||||||
PROCESSED_VALUE = 'true'
|
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()
|
from_h = (parsed.get('From') or '').lower()
|
||||||
auto_sub = (parsed.get('Auto-Submitted') or '').lower()
|
auto_sub = (parsed.get('Auto-Submitted') or '').lower()
|
||||||
return (
|
|
||||||
'mailer-daemon@us-east-2.amazonses.com' in from_h
|
# SES MAILER-DAEMON oder Auto-Submitted Header
|
||||||
and 'auto-replied' in auto_sub
|
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:
|
def domain_to_bucket(domain: str) -> str:
|
||||||
"""Konvertiert Domain zu S3 Bucket Namen"""
|
"""Konvertiert Domain zu S3 Bucket Namen"""
|
||||||
|
|
@ -159,7 +194,7 @@ def send_to_queue(queue_url: str, bucket: str, key: str,
|
||||||
'bucket': bucket,
|
'bucket': bucket,
|
||||||
'key': key,
|
'key': key,
|
||||||
'from': from_addr,
|
'from': from_addr,
|
||||||
'recipients': recipients, # Liste aller Empfänger
|
'recipients': recipients,
|
||||||
'domain': domain,
|
'domain': domain,
|
||||||
'subject': subject,
|
'subject': subject,
|
||||||
'message_id': message_id,
|
'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"✓ Queued to {queue_name}: SQS MessageId={sqs_message_id}")
|
||||||
print(f" Recipients: {len(recipients)} - {', '.join(recipients)}")
|
print(f" Recipients: {len(recipients)} - {', '.join(recipients)}")
|
||||||
|
|
||||||
# Als queued markieren
|
|
||||||
mark_as_queued(bucket, key, queue_name)
|
mark_as_queued(bucket, key, queue_name)
|
||||||
|
|
||||||
return sqs_message_id
|
return sqs_message_id
|
||||||
|
|
@ -206,8 +240,7 @@ def send_to_queue(queue_url: str, bucket: str, key: str,
|
||||||
|
|
||||||
def lambda_handler(event, context):
|
def lambda_handler(event, context):
|
||||||
"""
|
"""
|
||||||
Lambda Handler für SES Events
|
Lambda Handler für SES Inbound Events
|
||||||
Eine Domain pro Event = eine Queue Message mit allen Recipients
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
print(f"{'='*70}")
|
print(f"{'='*70}")
|
||||||
|
|
@ -221,10 +254,7 @@ def lambda_handler(event, context):
|
||||||
ses = record['ses']
|
ses = record['ses']
|
||||||
except (KeyError, IndexError) as e:
|
except (KeyError, IndexError) as e:
|
||||||
print(f"✗ Invalid event structure: {e}")
|
print(f"✗ Invalid event structure: {e}")
|
||||||
return {
|
return {'statusCode': 400, 'body': json.dumps({'error': 'Invalid SES event'})}
|
||||||
'statusCode': 400,
|
|
||||||
'body': json.dumps({'error': 'Invalid SES event'})
|
|
||||||
}
|
|
||||||
|
|
||||||
mail = ses['mail']
|
mail = ses['mail']
|
||||||
receipt = ses['receipt']
|
receipt = ses['receipt']
|
||||||
|
|
@ -234,21 +264,14 @@ def lambda_handler(event, context):
|
||||||
timestamp = mail.get('timestamp', '')
|
timestamp = mail.get('timestamp', '')
|
||||||
recipients = receipt.get('recipients', [])
|
recipients = receipt.get('recipients', [])
|
||||||
|
|
||||||
# FRÜHES LOGGING: S3 Key und Recipients
|
|
||||||
print(f"\n🔑 S3 Key: {message_id}")
|
print(f"\n🔑 S3 Key: {message_id}")
|
||||||
print(f"👥 Recipients ({len(recipients)}): {', '.join(recipients)}")
|
print(f"👥 Recipients ({len(recipients)}): {', '.join(recipients)}")
|
||||||
|
|
||||||
if not recipients:
|
if not recipients:
|
||||||
print(f"✗ No recipients found in event")
|
print(f"✗ No recipients found in event")
|
||||||
return {
|
return {'statusCode': 400, 'body': json.dumps({'error': 'No recipients'})}
|
||||||
'statusCode': 400,
|
|
||||||
'body': json.dumps({
|
|
||||||
'error': 'No recipients in event',
|
|
||||||
'message_id': message_id
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
# Domain extrahieren (alle Recipients haben gleiche Domain!)
|
# Domain extrahieren
|
||||||
domain = recipients[0].split('@')[1].lower()
|
domain = recipients[0].split('@')[1].lower()
|
||||||
bucket = domain_to_bucket(domain)
|
bucket = domain_to_bucket(domain)
|
||||||
|
|
||||||
|
|
@ -257,187 +280,129 @@ def lambda_handler(event, context):
|
||||||
print(f" From: {source}")
|
print(f" From: {source}")
|
||||||
print(f" Domain: {domain}")
|
print(f" Domain: {domain}")
|
||||||
print(f" Bucket: {bucket}")
|
print(f" Bucket: {bucket}")
|
||||||
print(f" Timestamp: {timestamp}")
|
|
||||||
print(f" Recipients: {len(recipients)}")
|
|
||||||
|
|
||||||
# Queue für Domain ermitteln
|
# Queue ermitteln
|
||||||
try:
|
try:
|
||||||
queue_url = get_queue_url_for_domain(domain)
|
queue_url = get_queue_url_for_domain(domain)
|
||||||
queue_name = queue_url.split('/')[-1]
|
queue_name = queue_url.split('/')[-1]
|
||||||
print(f" Queue: {queue_name}")
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"\n✗ ERROR: {e}")
|
print(f"\n✗ Queue ERROR: {e}")
|
||||||
return {
|
return {'statusCode': 500, 'body': json.dumps({'error': str(e)})}
|
||||||
'statusCode': 500,
|
|
||||||
'body': json.dumps({
|
|
||||||
'error': 'queue_not_configured',
|
|
||||||
'domain': domain,
|
|
||||||
'recipients': recipients,
|
|
||||||
'message': str(e)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
# S3 Object finden
|
# S3 Object finden
|
||||||
try:
|
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']:
|
if 'Contents' not in response:
|
||||||
raise Exception(f"No S3 object found for message {message_id}")
|
raise Exception(f"No S3 object found for {message_id}")
|
||||||
|
|
||||||
key = response['Contents'][0]['Key']
|
key = response['Contents'][0]['Key']
|
||||||
size = response['Contents'][0]['Size']
|
size = response['Contents'][0]['Size']
|
||||||
print(f" Found: s3://{bucket}/{key}")
|
print(f" Found: s3://{bucket}/{key} ({size/1024:.1f} KB)")
|
||||||
print(f" Size: {size:,} bytes ({size/1024:.1f} KB)")
|
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"\n✗ S3 ERROR: {e}")
|
print(f"\n✗ S3 ERROR: {e}")
|
||||||
return {
|
return {'statusCode': 404, 'body': json.dumps({'error': str(e)})}
|
||||||
'statusCode': 404,
|
|
||||||
'body': json.dumps({
|
|
||||||
'error': 's3_object_not_found',
|
|
||||||
'message_id': message_id,
|
|
||||||
'bucket': bucket,
|
|
||||||
'details': str(e)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
# Duplicate Check
|
# Duplicate Check
|
||||||
print(f"\n🔍 Checking for duplicates...")
|
|
||||||
if is_already_processed(bucket, key):
|
if is_already_processed(bucket, key):
|
||||||
print(f" Already processed, skipping")
|
return {'statusCode': 200, 'body': json.dumps({'status': 'already_processed'})}
|
||||||
return {
|
|
||||||
'statusCode': 200,
|
|
||||||
'body': json.dumps({
|
|
||||||
'status': 'already_processed',
|
|
||||||
'message_id': message_id,
|
|
||||||
'recipients': recipients
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
# Processing Lock setzen
|
# Processing Lock
|
||||||
print(f"\n🔒 Setting processing lock...")
|
|
||||||
if not set_processing_lock(bucket, key):
|
if not set_processing_lock(bucket, key):
|
||||||
print(f" Already being processed by another instance")
|
return {'statusCode': 200, 'body': json.dumps({'status': 'already_processing'})}
|
||||||
return {
|
|
||||||
'statusCode': 200,
|
|
||||||
'body': json.dumps({
|
|
||||||
'status': 'already_processing',
|
|
||||||
'message_id': message_id,
|
|
||||||
'recipients': recipients
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
# E-Mail laden um Subject zu extrahieren
|
# E-Mail laden und ggf. umschreiben
|
||||||
subject = '(unknown)'
|
subject = '(unknown)'
|
||||||
raw_bytes = b''
|
|
||||||
parsed = None
|
|
||||||
modified = False
|
modified = False
|
||||||
|
|
||||||
try:
|
try:
|
||||||
print(f"\n📖 Reading email for metadata...")
|
print(f"\n📖 Reading email...")
|
||||||
obj = s3.get_object(Bucket=bucket, Key=key)
|
obj = s3.get_object(Bucket=bucket, Key=key)
|
||||||
raw_bytes = obj['Body'].read()
|
raw_bytes = obj['Body'].read()
|
||||||
metadata = obj.get('Metadata', {}) or {}
|
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}" <no-reply@{domain}>')
|
|
||||||
|
|
||||||
# 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(
|
sqs_message_id = send_to_queue(
|
||||||
queue_url=queue_url,
|
queue_url=queue_url,
|
||||||
bucket=bucket,
|
bucket=bucket,
|
||||||
key=key,
|
key=key,
|
||||||
from_addr=source,
|
from_addr=source,
|
||||||
recipients=recipients, # ALLE Recipients
|
recipients=recipients,
|
||||||
domain=domain,
|
domain=domain,
|
||||||
subject=subject,
|
subject=subject,
|
||||||
message_id=message_id
|
message_id=message_id
|
||||||
)
|
)
|
||||||
|
|
||||||
print(f"\n{'='*70}")
|
print(f"\n✅ SUCCESS - Queued for delivery\n")
|
||||||
print(f"✅ SUCCESS - Email queued for delivery")
|
|
||||||
print(f"{'='*70}\n")
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
'statusCode': 200,
|
'statusCode': 200,
|
||||||
|
|
@ -445,26 +410,10 @@ def lambda_handler(event, context):
|
||||||
'status': 'queued',
|
'status': 'queued',
|
||||||
'message_id': message_id,
|
'message_id': message_id,
|
||||||
'sqs_message_id': sqs_message_id,
|
'sqs_message_id': sqs_message_id,
|
||||||
'queue': queue_name,
|
'modified': modified
|
||||||
'domain': domain,
|
|
||||||
'recipients': recipients,
|
|
||||||
'recipient_count': len(recipients),
|
|
||||||
'subject': subject
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"\n{'='*70}")
|
print(f"\n✗ QUEUE FAILED: {e}")
|
||||||
print(f"✗ FAILED TO QUEUE")
|
return {'statusCode': 500, 'body': json.dumps({'error': str(e)})}
|
||||||
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
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
@ -5,18 +5,20 @@ dynamo = boto3.resource('dynamodb', region_name='us-east-2')
|
||||||
table = dynamo.Table('ses-outbound-messages')
|
table = dynamo.Table('ses-outbound-messages')
|
||||||
|
|
||||||
def lambda_handler(event, context):
|
def lambda_handler(event, context):
|
||||||
detail = event['detail']
|
print(f"Received event: {event}")
|
||||||
mail = detail['mail']
|
|
||||||
msg_id = mail['messageId']
|
detail = event.get('detail', {})
|
||||||
source = mail['source']
|
mail = detail.get('mail', {})
|
||||||
destinations = mail['destination'] # Liste
|
msg_id = mail.get('messageId')
|
||||||
|
|
||||||
if not msg_id:
|
if not msg_id:
|
||||||
print("No MessageId in event")
|
print("No MessageId in event")
|
||||||
return
|
return
|
||||||
|
|
||||||
if event_type == 'SEND':
|
# Event-Type aus dem Event extrahieren
|
||||||
# wie bisher
|
event_type = detail.get('eventType')
|
||||||
|
|
||||||
|
if event_type == 'Send':
|
||||||
source = mail.get('source')
|
source = mail.get('source')
|
||||||
destinations = mail.get('destination', [])
|
destinations = mail.get('destination', [])
|
||||||
table.put_item(
|
table.put_item(
|
||||||
|
|
@ -27,9 +29,10 @@ def lambda_handler(event, context):
|
||||||
'timestamp': mail.get('timestamp')
|
'timestamp': mail.get('timestamp')
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
print(f"Stored SEND event for {msg_id}")
|
||||||
return
|
return
|
||||||
|
|
||||||
if event_type == 'BOUNCE':
|
if event_type == 'Bounce':
|
||||||
bounce = detail.get('bounce', {})
|
bounce = detail.get('bounce', {})
|
||||||
bounced = [
|
bounced = [
|
||||||
r.get('emailAddress')
|
r.get('emailAddress')
|
||||||
|
|
@ -40,18 +43,17 @@ def lambda_handler(event, context):
|
||||||
print("No bouncedRecipients in bounce event")
|
print("No bouncedRecipients in bounce event")
|
||||||
return
|
return
|
||||||
|
|
||||||
# in DynamoDB anhängen
|
|
||||||
table.update_item(
|
table.update_item(
|
||||||
Key={'MessageId': msg_id},
|
Key={'MessageId': msg_id},
|
||||||
UpdateExpression="ADD bouncedRecipients :b",
|
UpdateExpression="ADD bouncedRecipients :b",
|
||||||
ExpressionAttributeValues={
|
ExpressionAttributeValues={
|
||||||
':b': set(bounced) # String-Set
|
':b': set(bounced)
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
print(f"Updated {msg_id} with bouncedRecipients={bounced}")
|
print(f"Updated {msg_id} with bouncedRecipients={bounced}")
|
||||||
return
|
return
|
||||||
|
|
||||||
if event_type == 'COMPLAINT':
|
if event_type == 'Complaint':
|
||||||
complaint = detail.get('complaint', {})
|
complaint = detail.get('complaint', {})
|
||||||
complained = [
|
complained = [
|
||||||
r.get('emailAddress')
|
r.get('emailAddress')
|
||||||
|
|
@ -69,4 +71,4 @@ def lambda_handler(event, context):
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
print(f"Updated {msg_id} with complaintRecipients={complained}")
|
print(f"Updated {msg_id} with complaintRecipients={complained}")
|
||||||
return
|
return
|
||||||
|
|
@ -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
|
||||||
Loading…
Reference in New Issue