Skip to content

Commit

Permalink
feat: ses bounce emails lambda (#5926)
Browse files Browse the repository at this point in the history
* feat: ses bounce emails lambda

* Update ses_bounce.tf

* Update ses_bounce.tf

* add ses email logic

* Update bastion_linux.tf

* Update ses_bounce.tf

* Update ses_bounce.tf

* Update ses_bounce.tf

* Update ses_bounce.tf

* Update bounce_email_notification.py

* roles

* Update ses_logging.tf

* correct headers

* add jitbit dev provided html template

* Delete email_template.html

* add ignore tag

* Update ses_bounce.tf

* rename
  • Loading branch information
georgepstaylor authored May 10, 2024
1 parent b92f215 commit bef3ee5
Show file tree
Hide file tree
Showing 8 changed files with 260 additions and 33 deletions.
1 change: 0 additions & 1 deletion terraform/environments/delius-core/dms_iam.tf
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@

data "aws_iam_policy_document" "dms_assume_role" {
statement {
actions = ["sts:AssumeRole"]
Expand Down
10 changes: 5 additions & 5 deletions terraform/environments/delius-jitbit/application_variables.json
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@
"db_delete_automated_backups": "false",
"db_skip_final_snapshot": "true",
"db_snapshot_identifier": "rds:delius-jitbit-development-database-2024-03-01-06-19",
"ses_log_retention_days": "1"
"lambda_log_retention_days": "1"
},
"development": {
"migration_source_account_id": "377957503799",
Expand All @@ -50,7 +50,7 @@
"db_delete_automated_backups": "false",
"db_skip_final_snapshot": "true",
"db_snapshot_identifier": "cr-jitbit-dev-db-snapshot-for-testing-migration-to-mp-copy",
"ses_log_retention_days": "14"
"lambda_log_retention_days": "14"
},
"test": {
"migration_source_account_id": "377957503799",
Expand All @@ -76,7 +76,7 @@
"db_delete_automated_backups": "false",
"db_skip_final_snapshot": "true",
"db_snapshot_identifier": "copied-jitbit-training-migration-mp",
"ses_log_retention_days": "14"
"lambda_log_retention_days": "14"
},
"preproduction": {
"migration_source_account_id": "377957503799",
Expand All @@ -102,7 +102,7 @@
"db_delete_automated_backups": "false",
"db_skip_final_snapshot": "true",
"db_snapshot_identifier": "jitbit-preproduction-migration-trial-mp-copy",
"ses_log_retention_days": "14"
"lambda_log_retention_days": "14"
},
"production": {
"migration_source_account_id": "097456858629",
Expand All @@ -127,7 +127,7 @@
"db_deletion_protection": "true",
"db_skip_final_snapshot": "false",
"db_delete_automated_backups": "false",
"ses_log_retention_days": "14"
"lambda_log_retention_days": "14"
}
}
}
4 changes: 1 addition & 3 deletions terraform/environments/delius-jitbit/bastion_linux.tf
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,7 @@ module "bastion_linux" {
}

# s3 - used for logs and user ssh public keys
bucket_name = "bastion"
bucket_versioning = true
bucket_force_destroy = true
bucket_name = "bastion"
# public keys
public_key_data = local.public_key_data.keys[local.environment]
# logs
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
import os
import boto3
import json
import time
from datetime import datetime
from botocore.exceptions import ClientError


def handler(event, context):
todays_date = datetime.now().strftime("%Y-%m-%d")

message = event["Records"][0]["Sns"]["Message"]
message_dict = json.loads(message)

mail = message_dict.get("mail")
common_headers = mail.get("commonHeaders")
headers = mail.get("headers")

print(headers)

jitbit_ticket_id_arr = list(
filter(lambda id: id.get("name") == "X-Jitbit-TicketID", headers)
)
jitbit_ticket_id = jitbit_ticket_id_arr[0].get("value") if len(jitbit_ticket_id_arr) > 0 else "No Ticket ID Header"

subject = common_headers.get("subject")
reply_to = common_headers.get("replyTo")[0]
source = mail.get("source")

ses = boto3.client("sesv2")

bounce = message_dict.get("bounce")

bounced_recipients = bounce.get("bouncedRecipients")

bounced_recipients_message = ""
for bounced_recipient in bounced_recipients:
bounced_recipients_message += f""" <p>
Action: <strong>{bounced_recipient.get("action")}</strong>
<br>
</p>
<p>
Recipient: <strong>{bounced_recipient.get("emailAddress")}</strong>
<br>
</p>
<p>
Status: <strong>{bounced_recipient.get("status")}</strong>
<br>
</p>
<p>
Diagnostic Code: <strong>{bounced_recipient.get("diagnosticCode")}</strong>
</p>
"""

try:
email = ses.send_email(
FromEmailAddress=source,
Destination={"ToAddresses": [reply_to]},
ReplyToAddresses=[reply_to],
Content={
"Simple": {
"Subject": {"Data": f"BOUNCE <{jitbit_ticket_id}>: {subject} #tech#"},
"Body": {
"Html": {
"Data": f"""
<table width="100%" style="background-color: rgb(250, 232, 205);">
<tbody>
<tr>
<td style="background-color: rgb(255, 50, 50);"></td>
<td>&nbsp;&nbsp;</td>
<td width="100%">
<br>
<strong>WARNING:</strong>
<br>
<p> An email notification from this ticket has failed to deliver to one or more recipients. This may be the result of a 'Reply' or a 'Forward Ticket By Email'. See the diagnostic information below for more details. We have provided a simple guide <a href="https://helpdesk.jitbit.cr.probation.service.justice.gov.uk/KB/View/976525-">HERE</a> which explains the actions that you need to take.
<br>
</p>
</td>
</tr>
</tbody>
</table>
<br>
<hr>
<br>
<strong>DIAGNOSTIC INFORMATION:</strong>
<br>
<br>
<table width="100%" style="background-color: rgb(240, 240, 240);">
<tbody>
<tr>
<td style="background-color: rgb(150, 150, 150);"></td>
<td>&nbsp;&nbsp;</td>
<td width="100%">
<br>
<p>Ticket ID: <strong>{jitbit_ticket_id}</strong>
</p>
<p>Feedback ID: <strong>010b018f3d5323b8-1afeb777-87b9-4433-8104-f2f265151de6-000000</strong>
<br>
</p>
<p>Timestamp: <strong>2024-05-03T07:20:10.035Z</strong>
<br>
</p>
</tr>
<tr>
<td style="background-color: rgb(150, 150, 150);"></td>
<td>&nbsp;&nbsp;</td>
<td>
<p><strong>Bounced Recipients:</strong></p>
</td>
</tr>
<tr>
<td style="background-color: rgb(150, 150, 150);"></td>
<td>&nbsp;&nbsp;</td>
<td width="100%">
{bounced_recipients_message}
</td>
</tr>
</tbody>
</table>
<br>
<br>
"""
}
},
}
},
)

print(f"Email sent: {email}")

except ClientError as e:
print(e)
11 changes: 11 additions & 0 deletions terraform/environments/delius-jitbit/ses.tf
Original file line number Diff line number Diff line change
Expand Up @@ -90,3 +90,14 @@ resource "aws_ssm_parameter" "jitbit_ses_smtp_user" {
})
}

resource "aws_sesv2_configuration_set" "jitbit_ses_configuration_set" {
configuration_set_name = format("%s-configuration-set", local.application_name)

suppression_options {
suppressed_reasons = [
"COMPLAINT"
]
}

tags = local.tags
}
96 changes: 96 additions & 0 deletions terraform/environments/delius-jitbit/ses_bounce.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
resource "aws_sns_topic" "jitbit_ses_destination_topic_bounce_email_notification" {
name = format("%s-ses-destination-topic-bounce-email-notification", local.application_name)

tags = local.tags
}

resource "aws_sesv2_configuration_set_event_destination" "jitbit_ses_event_destination_bounce_email_notification" {
configuration_set_name = aws_sesv2_configuration_set.jitbit_ses_configuration_set.configuration_set_name
event_destination_name = format("%s-event-destination-bounce-email-notification", local.application_name)

event_destination {
sns_destination {
topic_arn = aws_sns_topic.jitbit_ses_destination_topic_bounce_email_notification.arn
}
enabled = true
matching_event_types = [
"BOUNCE"
]
}
}

data "archive_file" "lambda_function_payload_bounce_email_notification" {
type = "zip"
source_dir = "${path.module}/lambda/bounce_email_notification/"
output_path = "${path.module}/lambda/bounce_email_notification/bounce_email_notification.zip"
excludes = ["bounce_email_notification.zip"]
}

resource "aws_lambda_function" "bounce_email_notification" {
filename = "${path.module}/lambda/bounce_email_notification/bounce_email_notification.zip"
function_name = "bounce_email_notification"
architectures = ["arm64"]
role = aws_iam_role.lambda_bounce_email_notification.arn
runtime = "python3.12"
handler = "bounce_email_notification.handler"
source_code_hash = data.archive_file.lambda_function_payload_bounce_email_notification.output_base64sha256

lifecycle {
replace_triggered_by = [aws_iam_role.lambda_bounce_email_notification]
}

tags = local.tags
}

resource "aws_iam_role" "lambda_bounce_email_notification" {
name = "bounce_email_notification-role"
assume_role_policy = data.aws_iam_policy_document.lambda_assume_role_policy.json

tags = local.tags
}

resource "aws_iam_role_policy" "lambda_bounce_email_notification" {
name = "lambda_bounce_email_notification"
role = aws_iam_role.lambda_bounce_email_notification.id
policy = data.aws_iam_policy_document.lambda_policy_bounce_email_notification.json
}

data "aws_iam_policy_document" "lambda_policy_bounce_email_notification" {
statement {
actions = [
"ses:SendRawEmail",
"ses:SendEmail"
]
resources = ["*"]
}

statement {
actions = [
"logs:CreateLogGroup",
"logs:CreateLogStream",
"logs:PutLogEvents"
]
resources = ["arn:aws:logs:*:*:*"]
}
}

resource "aws_lambda_permission" "sns_bounce_email_notification" {
statement_id = "AllowExecutionFromSNS"
action = "lambda:InvokeFunction"
function_name = aws_lambda_function.bounce_email_notification.function_name
principal = "sns.amazonaws.com"
source_arn = aws_sns_topic.jitbit_ses_destination_topic_bounce_email_notification.arn
}

resource "aws_sns_topic_subscription" "lambda_bounce_email_notification" {
topic_arn = aws_sns_topic.jitbit_ses_destination_topic_bounce_email_notification.arn
protocol = "lambda"
endpoint = aws_lambda_function.bounce_email_notification.arn
}

resource "aws_cloudwatch_log_group" "bounce_email_notification" {
name = "/aws/lambda/bounce_email_notification"
retention_in_days = local.application_data.accounts[local.environment].lambda_log_retention_days

tags = local.tags
}
Original file line number Diff line number Diff line change
@@ -1,19 +1,6 @@
#####################
# SES Logging
#####################

resource "aws_sesv2_configuration_set" "jitbit_ses_configuration_set" {
configuration_set_name = format("%s-configuration-set", local.application_name)

suppression_options {
suppressed_reasons = [
"COMPLAINT"
]
}

tags = local.tags
}

resource "aws_sns_topic" "jitbit_ses_destination_topic" {
name = format("%s-ses-destination-topic", local.application_name)

Expand Down Expand Up @@ -41,16 +28,16 @@ resource "aws_sesv2_configuration_set_event_destination" "jitbit_ses_event_desti

data "archive_file" "lambda_function_payload" {
type = "zip"
source_dir = "${path.module}/lambda"
output_path = "${path.module}/lambda/sns_to_cloudwatch.zip"
source_dir = "${path.module}/lambda/sns_to_cloudwatch"
output_path = "${path.module}/lambda/sns_to_cloudwatch/sns_to_cloudwatch.zip"
excludes = ["sns_to_cloudwatch.zip"]
}

resource "aws_lambda_function" "sns_to_cloudwatch" {
filename = "${path.module}/lambda/sns_to_cloudwatch.zip"
filename = "${path.module}/lambda/sns_to_cloudwatch/sns_to_cloudwatch.zip"
function_name = "sns_to_cloudwatch"
architectures = ["arm64"]
role = aws_iam_role.lambda.arn
role = aws_iam_role.lambda_logging.arn
runtime = "python3.12"
handler = "sns_to_cloudwatch.handler"
source_code_hash = data.archive_file.lambda_function_payload.output_base64sha256
Expand All @@ -61,12 +48,16 @@ resource "aws_lambda_function" "sns_to_cloudwatch" {
}
}

lifecycle {
replace_triggered_by = [aws_iam_role.lambda_logging]
}

tags = local.tags
}

resource "aws_cloudwatch_log_group" "sns_logs" {
name = format("%s-ses-logs", local.application_name)
retention_in_days = local.application_data.accounts[local.environment].ses_log_retention_days
retention_in_days = local.application_data.accounts[local.environment].lambda_log_retention_days

tags = local.tags
}
Expand All @@ -78,7 +69,7 @@ resource "aws_cloudwatch_log_group" "execution_logs" {
tags = local.tags
}

resource "aws_iam_role" "lambda" {
resource "aws_iam_role" "lambda_logging" {
name = "sns_to_cloudwatch-role"
assume_role_policy = data.aws_iam_policy_document.lambda_assume_role_policy.json

Expand All @@ -95,13 +86,13 @@ data "aws_iam_policy_document" "lambda_assume_role_policy" {
}
}

resource "aws_iam_role_policy" "lambda" {
name = "lambda"
role = aws_iam_role.lambda.id
policy = data.aws_iam_policy_document.lambda_policy.json
resource "aws_iam_role_policy" "lambda_logging" {
name = "lambda-logging"
role = aws_iam_role.lambda_logging.id
policy = data.aws_iam_policy_document.lambda_logging__policy.json
}

data "aws_iam_policy_document" "lambda_policy" {
data "aws_iam_policy_document" "lambda_logging__policy" {
statement {
actions = [
"logs:CreateLogGroup",
Expand Down

0 comments on commit bef3ee5

Please sign in to comment.