2022-05-07 01:18:30 +02:00
|
|
|
#!/usr/bin/env python3
|
|
|
|
import os
|
|
|
|
import sys
|
2022-07-27 23:33:49 +02:00
|
|
|
from email.headerregistry import Address
|
2023-02-02 19:48:22 +01:00
|
|
|
from email.utils import parseaddr
|
2022-05-07 01:18:30 +02:00
|
|
|
|
|
|
|
sys.path.insert(0, os.path.join(os.path.dirname(__file__), ".."))
|
|
|
|
from scripts.lib.setup_path import setup_path
|
|
|
|
|
|
|
|
setup_path()
|
|
|
|
|
|
|
|
os.environ["DJANGO_SETTINGS_MODULE"] = "zproject.settings"
|
|
|
|
import argparse
|
|
|
|
import secrets
|
|
|
|
from contextlib import contextmanager
|
2022-07-15 16:17:12 +02:00
|
|
|
from typing import Iterator, List, Tuple, TypedDict
|
2022-05-07 01:18:30 +02:00
|
|
|
|
2022-07-30 07:09:07 +02:00
|
|
|
import boto3.session
|
2022-05-07 01:18:30 +02:00
|
|
|
import orjson
|
|
|
|
from django.conf import settings
|
|
|
|
from mypy_boto3_ses import SESClient
|
|
|
|
from mypy_boto3_sns import SNSClient
|
|
|
|
from mypy_boto3_sqs import SQSClient
|
|
|
|
from mypy_boto3_sqs.type_defs import DeleteMessageBatchRequestEntryTypeDef
|
|
|
|
|
|
|
|
|
2022-07-15 16:17:12 +02:00
|
|
|
class IdentityArgsDict(TypedDict, total=False):
|
|
|
|
default: str
|
|
|
|
required: bool
|
|
|
|
|
|
|
|
|
2022-05-07 01:18:30 +02:00
|
|
|
def main() -> None:
|
2023-02-02 19:47:55 +01:00
|
|
|
session = boto3.session.Session(region_name=settings.S3_REGION)
|
2022-05-07 01:18:30 +02:00
|
|
|
|
2023-02-02 19:48:22 +01:00
|
|
|
# Strip off the realname, if present, and extract just the domain name
|
|
|
|
_, from_address = parseaddr(settings.NOREPLY_EMAIL_ADDRESS)
|
2022-07-27 23:33:49 +02:00
|
|
|
from_host = Address(addr_spec=from_address).domain
|
2022-05-07 01:18:30 +02:00
|
|
|
|
|
|
|
ses: SESClient = session.client("ses")
|
|
|
|
possible_identities = []
|
|
|
|
identity_paginator = ses.get_paginator("list_identities")
|
|
|
|
for identity_resp in identity_paginator.paginate():
|
|
|
|
possible_identities += identity_resp["Identities"]
|
|
|
|
|
2022-07-15 16:17:12 +02:00
|
|
|
identity_args: IdentityArgsDict = {}
|
2022-05-07 01:18:30 +02:00
|
|
|
if from_host in possible_identities:
|
|
|
|
identity_args["default"] = from_host
|
|
|
|
elif from_address in possible_identities:
|
|
|
|
identity_args["default"] = from_address
|
|
|
|
else:
|
|
|
|
identity_args["required"] = True
|
|
|
|
|
|
|
|
parser = argparse.ArgumentParser(description="Tail SES delivery or bounces")
|
|
|
|
parser.add_argument(
|
|
|
|
"--identity",
|
|
|
|
"-i",
|
|
|
|
choices=possible_identities,
|
|
|
|
help="Sending identity in SES",
|
|
|
|
**identity_args,
|
|
|
|
)
|
|
|
|
topic_group = parser.add_mutually_exclusive_group(required=True)
|
|
|
|
topic_group.add_argument("--bounces", "-b", action="store_true")
|
|
|
|
topic_group.add_argument("--deliveries", "-d", action="store_true")
|
|
|
|
topic_group.add_argument("--complaints", "-c", action="store_true")
|
|
|
|
args = parser.parse_args()
|
|
|
|
|
|
|
|
sns_topic_arn = get_ses_arn(session, args)
|
|
|
|
with our_sqs_queue(session, sns_topic_arn) as (queue_arn, queue_url):
|
|
|
|
with our_sns_subscription(session, sns_topic_arn, queue_arn):
|
|
|
|
print_messages(session, queue_url)
|
|
|
|
|
|
|
|
|
2022-07-30 07:09:07 +02:00
|
|
|
def get_ses_arn(session: boto3.session.Session, args: argparse.Namespace) -> str:
|
2022-05-07 01:18:30 +02:00
|
|
|
ses: SESClient = session.client("ses")
|
|
|
|
|
|
|
|
notification_settings = ses.get_identity_notification_attributes(Identities=[args.identity])
|
|
|
|
settings = notification_settings["NotificationAttributes"][args.identity]
|
|
|
|
|
|
|
|
if args.bounces:
|
|
|
|
return settings["BounceTopic"]
|
|
|
|
elif args.complaints:
|
|
|
|
return settings["ComplaintTopic"]
|
|
|
|
elif args.deliveries:
|
|
|
|
return settings["DeliveryTopic"]
|
2022-10-30 00:12:04 +02:00
|
|
|
raise AssertionError() # Unreachable
|
2022-05-07 01:18:30 +02:00
|
|
|
|
|
|
|
|
|
|
|
@contextmanager
|
2022-07-30 07:09:07 +02:00
|
|
|
def our_sqs_queue(session: boto3.session.Session, ses_topic_arn: str) -> Iterator[Tuple[str, str]]:
|
2022-05-07 01:18:30 +02:00
|
|
|
(_, _, _, region, account_id, topic_name) = ses_topic_arn.split(":")
|
|
|
|
|
|
|
|
sqs: SQSClient = session.client("sqs")
|
|
|
|
queue_name = "tail-ses-" + secrets.token_hex(10)
|
|
|
|
try:
|
|
|
|
resp = sqs.create_queue(
|
|
|
|
QueueName=queue_name,
|
|
|
|
Attributes={
|
|
|
|
"Policy": orjson.dumps(
|
|
|
|
{
|
|
|
|
"Version": "2012-10-17",
|
|
|
|
"Id": secrets.token_hex(10),
|
|
|
|
"Statement": [
|
|
|
|
{
|
|
|
|
"Sid": "Sid" + secrets.token_hex(10),
|
|
|
|
"Effect": "Allow",
|
|
|
|
"Principal": {"AWS": "*"},
|
|
|
|
"Action": "SQS:SendMessage",
|
|
|
|
"Resource": f"arn:aws:sqs:{region}:{account_id}:{queue_name}",
|
|
|
|
"Condition": {"ArnEquals": {"aws:SourceArn": ses_topic_arn}},
|
|
|
|
}
|
|
|
|
],
|
|
|
|
}
|
|
|
|
).decode("UTF-8")
|
|
|
|
},
|
|
|
|
)
|
|
|
|
queue_url = resp["QueueUrl"]
|
|
|
|
yield f"arn:aws:sqs:{region}:{account_id}:{queue_name}", queue_url
|
|
|
|
finally:
|
|
|
|
if queue_url is not None:
|
|
|
|
print("Deleting temporary queue...", file=sys.stderr)
|
|
|
|
sqs.delete_queue(QueueUrl=queue_url)
|
|
|
|
|
|
|
|
|
|
|
|
@contextmanager
|
|
|
|
def our_sns_subscription(
|
2022-07-30 07:09:07 +02:00
|
|
|
session: boto3.session.Session, ses_topic_arn: str, queue_arn: str
|
2022-05-07 01:18:30 +02:00
|
|
|
) -> Iterator[str]:
|
|
|
|
sns: SNSClient = session.client("sns")
|
|
|
|
try:
|
|
|
|
resp = sns.subscribe(
|
|
|
|
TopicArn=ses_topic_arn,
|
|
|
|
Protocol="sqs",
|
|
|
|
Endpoint=queue_arn,
|
|
|
|
Attributes={"RawMessageDelivery": "false"},
|
|
|
|
ReturnSubscriptionArn=True,
|
|
|
|
)
|
|
|
|
subscription_arn = resp["SubscriptionArn"]
|
|
|
|
yield subscription_arn
|
|
|
|
finally:
|
|
|
|
if subscription_arn is not None:
|
|
|
|
print("Deleting temporary SNS subscription...", file=sys.stderr)
|
|
|
|
sns.unsubscribe(SubscriptionArn=subscription_arn)
|
|
|
|
|
|
|
|
|
2022-07-30 07:09:07 +02:00
|
|
|
def print_messages(session: boto3.session.Session, queue_url: str) -> None:
|
2022-05-07 01:18:30 +02:00
|
|
|
sqs: SQSClient = session.client("sqs")
|
|
|
|
try:
|
|
|
|
while True:
|
|
|
|
resp = sqs.receive_message(
|
|
|
|
QueueUrl=queue_url,
|
|
|
|
MaxNumberOfMessages=10,
|
|
|
|
WaitTimeSeconds=5,
|
|
|
|
MessageAttributeNames=["All"],
|
|
|
|
)
|
|
|
|
messages = resp.get("Messages", [])
|
|
|
|
delete_list: List[DeleteMessageBatchRequestEntryTypeDef] = []
|
|
|
|
for m in messages:
|
|
|
|
body = orjson.loads(m["Body"])
|
|
|
|
body_message = orjson.loads(body["Message"])
|
|
|
|
print(
|
|
|
|
body["Timestamp"]
|
|
|
|
+ " "
|
|
|
|
+ orjson.dumps(body_message, option=orjson.OPT_INDENT_2).decode("utf-8")
|
|
|
|
)
|
|
|
|
delete_list.append({"Id": m["MessageId"], "ReceiptHandle": m["ReceiptHandle"]})
|
|
|
|
if delete_list:
|
|
|
|
sqs.delete_message_batch(QueueUrl=queue_url, Entries=delete_list)
|
|
|
|
except KeyboardInterrupt:
|
|
|
|
pass
|
|
|
|
|
|
|
|
|
|
|
|
if __name__ == "__main__":
|
|
|
|
main()
|