@everydaydevopsio/hail 0.0.0-stage → 0.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,95 @@
1
+ terraform {
2
+ required_version = ">= 1.7, < 2.0"
3
+ required_providers {
4
+ aws = { source = "hashicorp/aws", version = ">= 5.50, < 7.0" }
5
+ cloudflare = { source = "cloudflare/cloudflare", version = "~> 5.0" }
6
+ }
7
+ }
8
+ provider "aws" { region = var.region }
9
+ # Reads CLOUDFLARE_API_TOKEN from the environment. Do not put it in tfvars.
10
+ provider "cloudflare" {}
11
+ variable "name" { type = string }
12
+ variable "domain" { type = string }
13
+ variable "zone_name" { type = string }
14
+ variable "zone_id" { type = string }
15
+ variable "region" { type = string }
16
+ variable "existing_rule_set_name" {
17
+ type = string
18
+ default = null
19
+ }
20
+ variable "manage_rule_set_activation" {
21
+ type = bool
22
+ default = false
23
+ }
24
+ variable "retention_days" {
25
+ type = number
26
+ default = 3
27
+ }
28
+ variable "reader_principal_arns" {
29
+ type = list(string)
30
+ default = []
31
+ }
32
+ variable "external_indexer_role_arn" {
33
+ type = string
34
+ default = null
35
+ }
36
+ variable "external_reader_role_arn" {
37
+ type = string
38
+ default = null
39
+ }
40
+ variable "permissions_boundary_arn" {
41
+ type = string
42
+ default = null
43
+ }
44
+ variable "tags" {
45
+ type = map(string)
46
+ default = {}
47
+ }
48
+ data "cloudflare_zone" "selected" { zone_id = var.zone_id }
49
+ resource "terraform_data" "guard" {
50
+ input = var.domain
51
+ lifecycle {
52
+ precondition {
53
+ condition = data.cloudflare_zone.selected.name == var.zone_name && var.domain != var.zone_name && endswith(var.domain, ".${var.zone_name}")
54
+ error_message = "Use a dedicated subdomain of the selected existing zone; never the zone apex."
55
+ }
56
+ }
57
+ }
58
+ module "receiver" {
59
+ source = "../../modules/receiver"
60
+ name = var.name
61
+ domain = var.domain
62
+ region = var.region
63
+ existing_rule_set_name = var.existing_rule_set_name
64
+ manage_rule_set_activation = var.manage_rule_set_activation
65
+ retention_days = var.retention_days
66
+ reader_principal_arns = var.reader_principal_arns
67
+ external_indexer_role_arn = var.external_indexer_role_arn
68
+ external_reader_role_arn = var.external_reader_role_arn
69
+ permissions_boundary_arn = var.permissions_boundary_arn
70
+ tags = var.tags
71
+ depends_on = [terraform_data.guard]
72
+ }
73
+ resource "cloudflare_dns_record" "mx" {
74
+ zone_id = var.zone_id
75
+ name = var.domain
76
+ type = "MX"
77
+ content = module.receiver.mx_target
78
+ priority = 10
79
+ ttl = 300
80
+ }
81
+ resource "cloudflare_dns_record" "verification" {
82
+ zone_id = var.zone_id
83
+ name = "_amazonses.${var.domain}"
84
+ type = "TXT"
85
+ content = module.receiver.verification_token
86
+ ttl = 300
87
+ }
88
+ resource "aws_ses_domain_identity_verification" "receiver" {
89
+ domain = var.domain
90
+ depends_on = [cloudflare_dns_record.mx, cloudflare_dns_record.verification]
91
+ }
92
+ output "hail_config" { value = module.receiver.hail_config }
93
+ output "dns_records" { value = module.receiver.dns_records }
94
+ output "reader_policy_arn" { value = module.receiver.reader_policy_arn }
95
+ output "dlq_url" { value = module.receiver.dlq_url }
@@ -0,0 +1,47 @@
1
+ # This file is maintained automatically by "terraform init".
2
+ # Manual edits may be lost in future updates.
3
+
4
+ provider "registry.terraform.io/hashicorp/archive" {
5
+ version = "2.8.1"
6
+ constraints = ">= 2.4.0, < 3.0.0"
7
+ hashes = [
8
+ "h1:KfIRyazppfpvRKIW5URe4sIVRPPdcbtt4ddkYd1Bw3Y=",
9
+ "zh:03de290604114a89fcd45c2e5bc7787d5a1ebfc5f964fb5989306bea7a4c79ec",
10
+ "zh:0a7d69dc9fbbc48960bc2f04588c8fb1bd92c78a8f306566b7fb17fc4a4f2058",
11
+ "zh:4df1f3981379c35f1757da957470f7f7724496b57219da3485177cf1647bdf59",
12
+ "zh:50f0e72ba53bfe6e11b03b7fc899e1f72536354381d302a743a274ae2a3f45f4",
13
+ "zh:5c4e15a04c98e2a8cafb1cd9632b48ad318051e8462d105b1153006277985c35",
14
+ "zh:66069e604bcf5c4af0278e15997d9e6bd755c54fb3801d78885838b889729c5f",
15
+ "zh:78d5eefdd9e494defcb3c68d282b8f96630502cac21d1ea161f53cfe9bb483b3",
16
+ "zh:979765db3f42601870ab377104ba70befb029547b278337ec4ada980e3582de6",
17
+ "zh:b165254da774f49945a73fbccc3ef1b63d70ea00a98fa9e14716665ba80ecaad",
18
+ "zh:c0bb2697b525da9fec4511f569ed2bd2b42f25d5c1f9fa00f8f3645b50cfc2e9",
19
+ "zh:c48f6695d12df0d0fa5231f7c1b8d518a4feae91a733fdd35a5804af3a303831",
20
+ "zh:d589954c93f075180c9f4e2ce91780d5edcb56dfd0d3cda8ba08e13c10b52249",
21
+ "zh:f5792ed06da65d0daf7ca3711f5399ff78c7cb4400afe53fbce9a926fbde4477",
22
+ ]
23
+ }
24
+
25
+ provider "registry.terraform.io/hashicorp/aws" {
26
+ version = "6.66.0"
27
+ constraints = ">= 5.50.0, < 7.0.0"
28
+ hashes = [
29
+ "h1:hBEaeBm9nm7A/u1nnD0nfolTPP55/BoKRFWk8zG8/fk=",
30
+ "zh:156fe7164a3d26ef6b35734c43e99fb198df90575ed897d1182b8e930b8cd523",
31
+ "zh:1af52b22b35be00f8d16e3ebebff9fa699ec4db2ef69e6032ba5c536f80c03d9",
32
+ "zh:2545a8478bd551fdc9694f6cc1a1ad24617f6736f8bde0ad6cae90987c65380f",
33
+ "zh:4070db1ee369ccb41cb610bfd887386bc0a9b9ecad60aeb4dbce58443d2519dd",
34
+ "zh:53da7d3c1840ef875c7d34e967732502a64fe677af0e78824773d4c15a8fe740",
35
+ "zh:576a93a28bf611a4de2a2e6ced697a41d5126b8fd31d30782b16797e410a9706",
36
+ "zh:58fed5fa9a033355b9d4f3092c817b70d934100e0d8678d6e4c93f3c9493d4e4",
37
+ "zh:6a9ca2f24e2ee9156dd785d159a850b35d190e9cf7eca21cb9582970c2db80cd",
38
+ "zh:729edd30f99cc16009deba5c013265b0c81eda261a3d0821cbd011d3287fd230",
39
+ "zh:7ae460049b75bd4aefee465ef7c53a01ac2df46d4d3e3ac00824afa8b5cb83fb",
40
+ "zh:9051fa85c8034ade8a57a5c6f232fd33da28f3800bb5aa40bc8625dbc5e27632",
41
+ "zh:906547e4319805e7acf7fbdf2bac28a4b1a7370790a2a430c7adb1b29bb934eb",
42
+ "zh:998f27410a66158a35ee5ed142c27e5b21fe8601941da55da2157f8042d6dcca",
43
+ "zh:9b12af85486a96aedd8d7984b0ff811a4b42e3d88dad1a3fb4c0b580d04fa425",
44
+ "zh:9c1804eff1dda0446dc2d215231015bb65a2fc6c3b7ba24584fe45f1ddd3fa9f",
45
+ "zh:b03ff5efdee310502aaaeb460144dc059bce72a0d8217e6b989099ef8aef9283",
46
+ ]
47
+ }
@@ -0,0 +1,71 @@
1
+ terraform {
2
+ required_version = ">= 1.7, < 2.0"
3
+ required_providers { aws = { source = "hashicorp/aws", version = ">= 5.50, < 7.0" } }
4
+ }
5
+ provider "aws" { region = var.region }
6
+ variable "name" { type = string }
7
+ variable "domain" { type = string }
8
+ variable "zone_name" { type = string }
9
+ variable "region" { type = string }
10
+ variable "existing_rule_set_name" {
11
+ type = string
12
+ default = null
13
+ }
14
+ variable "manage_rule_set_activation" {
15
+ type = bool
16
+ default = false
17
+ }
18
+ variable "retention_days" {
19
+ type = number
20
+ default = 3
21
+ }
22
+ variable "reader_principal_arns" {
23
+ type = list(string)
24
+ default = []
25
+ }
26
+ variable "external_indexer_role_arn" {
27
+ type = string
28
+ default = null
29
+ }
30
+ variable "external_reader_role_arn" {
31
+ type = string
32
+ default = null
33
+ }
34
+ variable "permissions_boundary_arn" {
35
+ type = string
36
+ default = null
37
+ }
38
+ variable "tags" {
39
+ type = map(string)
40
+ default = {}
41
+ }
42
+ resource "terraform_data" "guard" {
43
+ input = var.domain
44
+ lifecycle {
45
+ precondition {
46
+ condition = var.domain != var.zone_name && endswith(var.domain, ".${var.zone_name}")
47
+ error_message = "Use a dedicated test subdomain; never change the company email domain."
48
+ }
49
+ }
50
+ }
51
+ module "receiver" {
52
+ source = "../../modules/receiver"
53
+ name = var.name
54
+ domain = var.domain
55
+ region = var.region
56
+ existing_rule_set_name = var.existing_rule_set_name
57
+ manage_rule_set_activation = var.manage_rule_set_activation
58
+ retention_days = var.retention_days
59
+ reader_principal_arns = var.reader_principal_arns
60
+ external_indexer_role_arn = var.external_indexer_role_arn
61
+ external_reader_role_arn = var.external_reader_role_arn
62
+ permissions_boundary_arn = var.permissions_boundary_arn
63
+ tags = var.tags
64
+ depends_on = [terraform_data.guard]
65
+ }
66
+ # Manual mode deliberately does not wait for DNS during apply.
67
+ # Publish dns_records, then run `hail doctor` to verify readiness.
68
+ output "hail_config" { value = module.receiver.hail_config }
69
+ output "dns_records" { value = module.receiver.dns_records }
70
+ output "reader_policy_arn" { value = module.receiver.reader_policy_arn }
71
+ output "dlq_url" { value = module.receiver.dlq_url }
@@ -0,0 +1,47 @@
1
+ # This file is maintained automatically by "terraform init".
2
+ # Manual edits may be lost in future updates.
3
+
4
+ provider "registry.terraform.io/hashicorp/archive" {
5
+ version = "2.8.1"
6
+ constraints = ">= 2.4.0, < 3.0.0"
7
+ hashes = [
8
+ "h1:KfIRyazppfpvRKIW5URe4sIVRPPdcbtt4ddkYd1Bw3Y=",
9
+ "zh:03de290604114a89fcd45c2e5bc7787d5a1ebfc5f964fb5989306bea7a4c79ec",
10
+ "zh:0a7d69dc9fbbc48960bc2f04588c8fb1bd92c78a8f306566b7fb17fc4a4f2058",
11
+ "zh:4df1f3981379c35f1757da957470f7f7724496b57219da3485177cf1647bdf59",
12
+ "zh:50f0e72ba53bfe6e11b03b7fc899e1f72536354381d302a743a274ae2a3f45f4",
13
+ "zh:5c4e15a04c98e2a8cafb1cd9632b48ad318051e8462d105b1153006277985c35",
14
+ "zh:66069e604bcf5c4af0278e15997d9e6bd755c54fb3801d78885838b889729c5f",
15
+ "zh:78d5eefdd9e494defcb3c68d282b8f96630502cac21d1ea161f53cfe9bb483b3",
16
+ "zh:979765db3f42601870ab377104ba70befb029547b278337ec4ada980e3582de6",
17
+ "zh:b165254da774f49945a73fbccc3ef1b63d70ea00a98fa9e14716665ba80ecaad",
18
+ "zh:c0bb2697b525da9fec4511f569ed2bd2b42f25d5c1f9fa00f8f3645b50cfc2e9",
19
+ "zh:c48f6695d12df0d0fa5231f7c1b8d518a4feae91a733fdd35a5804af3a303831",
20
+ "zh:d589954c93f075180c9f4e2ce91780d5edcb56dfd0d3cda8ba08e13c10b52249",
21
+ "zh:f5792ed06da65d0daf7ca3711f5399ff78c7cb4400afe53fbce9a926fbde4477",
22
+ ]
23
+ }
24
+
25
+ provider "registry.terraform.io/hashicorp/aws" {
26
+ version = "6.66.0"
27
+ constraints = ">= 5.50.0, < 7.0.0"
28
+ hashes = [
29
+ "h1:hBEaeBm9nm7A/u1nnD0nfolTPP55/BoKRFWk8zG8/fk=",
30
+ "zh:156fe7164a3d26ef6b35734c43e99fb198df90575ed897d1182b8e930b8cd523",
31
+ "zh:1af52b22b35be00f8d16e3ebebff9fa699ec4db2ef69e6032ba5c536f80c03d9",
32
+ "zh:2545a8478bd551fdc9694f6cc1a1ad24617f6736f8bde0ad6cae90987c65380f",
33
+ "zh:4070db1ee369ccb41cb610bfd887386bc0a9b9ecad60aeb4dbce58443d2519dd",
34
+ "zh:53da7d3c1840ef875c7d34e967732502a64fe677af0e78824773d4c15a8fe740",
35
+ "zh:576a93a28bf611a4de2a2e6ced697a41d5126b8fd31d30782b16797e410a9706",
36
+ "zh:58fed5fa9a033355b9d4f3092c817b70d934100e0d8678d6e4c93f3c9493d4e4",
37
+ "zh:6a9ca2f24e2ee9156dd785d159a850b35d190e9cf7eca21cb9582970c2db80cd",
38
+ "zh:729edd30f99cc16009deba5c013265b0c81eda261a3d0821cbd011d3287fd230",
39
+ "zh:7ae460049b75bd4aefee465ef7c53a01ac2df46d4d3e3ac00824afa8b5cb83fb",
40
+ "zh:9051fa85c8034ade8a57a5c6f232fd33da28f3800bb5aa40bc8625dbc5e27632",
41
+ "zh:906547e4319805e7acf7fbdf2bac28a4b1a7370790a2a430c7adb1b29bb934eb",
42
+ "zh:998f27410a66158a35ee5ed142c27e5b21fe8601941da55da2157f8042d6dcca",
43
+ "zh:9b12af85486a96aedd8d7984b0ff811a4b42e3d88dad1a3fb4c0b580d04fa425",
44
+ "zh:9c1804eff1dda0446dc2d215231015bb65a2fc6c3b7ba24584fe45f1ddd3fa9f",
45
+ "zh:b03ff5efdee310502aaaeb460144dc059bce72a0d8217e6b989099ef8aef9283",
46
+ ]
47
+ }
@@ -0,0 +1,94 @@
1
+ terraform {
2
+ required_version = ">= 1.7, < 2.0"
3
+ required_providers { aws = { source = "hashicorp/aws", version = ">= 5.50, < 7.0" } }
4
+ }
5
+ provider "aws" { region = var.region }
6
+ variable "name" { type = string }
7
+ variable "domain" { type = string }
8
+ variable "zone_name" { type = string }
9
+ variable "zone_id" { type = string }
10
+ variable "region" { type = string }
11
+ variable "existing_rule_set_name" {
12
+ type = string
13
+ default = null
14
+ }
15
+ variable "manage_rule_set_activation" {
16
+ type = bool
17
+ default = false
18
+ }
19
+ variable "retention_days" {
20
+ type = number
21
+ default = 3
22
+ }
23
+ variable "reader_principal_arns" {
24
+ type = list(string)
25
+ default = []
26
+ }
27
+ variable "external_indexer_role_arn" {
28
+ type = string
29
+ default = null
30
+ }
31
+ variable "external_reader_role_arn" {
32
+ type = string
33
+ default = null
34
+ }
35
+ variable "permissions_boundary_arn" {
36
+ type = string
37
+ default = null
38
+ }
39
+ variable "tags" {
40
+ type = map(string)
41
+ default = {}
42
+ }
43
+ data "aws_route53_zone" "selected" {
44
+ zone_id = var.zone_id
45
+ private_zone = false
46
+ }
47
+ resource "terraform_data" "guard" {
48
+ input = var.domain
49
+ lifecycle {
50
+ precondition {
51
+ condition = !data.aws_route53_zone.selected.private_zone && trimsuffix(data.aws_route53_zone.selected.name, ".") == var.zone_name && var.domain != var.zone_name && endswith(var.domain, ".${var.zone_name}")
52
+ error_message = "Use a dedicated subdomain of the selected PUBLIC hosted zone; never the zone apex."
53
+ }
54
+ }
55
+ }
56
+ module "receiver" {
57
+ source = "../../modules/receiver"
58
+ name = var.name
59
+ domain = var.domain
60
+ region = var.region
61
+ existing_rule_set_name = var.existing_rule_set_name
62
+ manage_rule_set_activation = var.manage_rule_set_activation
63
+ retention_days = var.retention_days
64
+ reader_principal_arns = var.reader_principal_arns
65
+ external_indexer_role_arn = var.external_indexer_role_arn
66
+ external_reader_role_arn = var.external_reader_role_arn
67
+ permissions_boundary_arn = var.permissions_boundary_arn
68
+ tags = var.tags
69
+ depends_on = [terraform_data.guard]
70
+ }
71
+ resource "aws_route53_record" "mx" {
72
+ zone_id = var.zone_id
73
+ name = var.domain
74
+ type = "MX"
75
+ ttl = 300
76
+ records = ["10 ${module.receiver.mx_target}"]
77
+ allow_overwrite = false
78
+ }
79
+ resource "aws_route53_record" "verification" {
80
+ zone_id = var.zone_id
81
+ name = "_amazonses.${var.domain}"
82
+ type = "TXT"
83
+ ttl = 300
84
+ records = [module.receiver.verification_token]
85
+ allow_overwrite = false
86
+ }
87
+ resource "aws_ses_domain_identity_verification" "receiver" {
88
+ domain = var.domain
89
+ depends_on = [aws_route53_record.mx, aws_route53_record.verification]
90
+ }
91
+ output "hail_config" { value = module.receiver.hail_config }
92
+ output "dns_records" { value = module.receiver.dns_records }
93
+ output "reader_policy_arn" { value = module.receiver.reader_policy_arn }
94
+ output "dlq_url" { value = module.receiver.dlq_url }
@@ -0,0 +1,47 @@
1
+ # This file is maintained automatically by "terraform init".
2
+ # Manual edits may be lost in future updates.
3
+
4
+ provider "registry.terraform.io/hashicorp/archive" {
5
+ version = "2.8.1"
6
+ constraints = ">= 2.4.0, < 3.0.0"
7
+ hashes = [
8
+ "h1:KfIRyazppfpvRKIW5URe4sIVRPPdcbtt4ddkYd1Bw3Y=",
9
+ "zh:03de290604114a89fcd45c2e5bc7787d5a1ebfc5f964fb5989306bea7a4c79ec",
10
+ "zh:0a7d69dc9fbbc48960bc2f04588c8fb1bd92c78a8f306566b7fb17fc4a4f2058",
11
+ "zh:4df1f3981379c35f1757da957470f7f7724496b57219da3485177cf1647bdf59",
12
+ "zh:50f0e72ba53bfe6e11b03b7fc899e1f72536354381d302a743a274ae2a3f45f4",
13
+ "zh:5c4e15a04c98e2a8cafb1cd9632b48ad318051e8462d105b1153006277985c35",
14
+ "zh:66069e604bcf5c4af0278e15997d9e6bd755c54fb3801d78885838b889729c5f",
15
+ "zh:78d5eefdd9e494defcb3c68d282b8f96630502cac21d1ea161f53cfe9bb483b3",
16
+ "zh:979765db3f42601870ab377104ba70befb029547b278337ec4ada980e3582de6",
17
+ "zh:b165254da774f49945a73fbccc3ef1b63d70ea00a98fa9e14716665ba80ecaad",
18
+ "zh:c0bb2697b525da9fec4511f569ed2bd2b42f25d5c1f9fa00f8f3645b50cfc2e9",
19
+ "zh:c48f6695d12df0d0fa5231f7c1b8d518a4feae91a733fdd35a5804af3a303831",
20
+ "zh:d589954c93f075180c9f4e2ce91780d5edcb56dfd0d3cda8ba08e13c10b52249",
21
+ "zh:f5792ed06da65d0daf7ca3711f5399ff78c7cb4400afe53fbce9a926fbde4477",
22
+ ]
23
+ }
24
+
25
+ provider "registry.terraform.io/hashicorp/aws" {
26
+ version = "6.66.0"
27
+ constraints = ">= 5.50.0, < 7.0.0"
28
+ hashes = [
29
+ "h1:hBEaeBm9nm7A/u1nnD0nfolTPP55/BoKRFWk8zG8/fk=",
30
+ "zh:156fe7164a3d26ef6b35734c43e99fb198df90575ed897d1182b8e930b8cd523",
31
+ "zh:1af52b22b35be00f8d16e3ebebff9fa699ec4db2ef69e6032ba5c536f80c03d9",
32
+ "zh:2545a8478bd551fdc9694f6cc1a1ad24617f6736f8bde0ad6cae90987c65380f",
33
+ "zh:4070db1ee369ccb41cb610bfd887386bc0a9b9ecad60aeb4dbce58443d2519dd",
34
+ "zh:53da7d3c1840ef875c7d34e967732502a64fe677af0e78824773d4c15a8fe740",
35
+ "zh:576a93a28bf611a4de2a2e6ced697a41d5126b8fd31d30782b16797e410a9706",
36
+ "zh:58fed5fa9a033355b9d4f3092c817b70d934100e0d8678d6e4c93f3c9493d4e4",
37
+ "zh:6a9ca2f24e2ee9156dd785d159a850b35d190e9cf7eca21cb9582970c2db80cd",
38
+ "zh:729edd30f99cc16009deba5c013265b0c81eda261a3d0821cbd011d3287fd230",
39
+ "zh:7ae460049b75bd4aefee465ef7c53a01ac2df46d4d3e3ac00824afa8b5cb83fb",
40
+ "zh:9051fa85c8034ade8a57a5c6f232fd33da28f3800bb5aa40bc8625dbc5e27632",
41
+ "zh:906547e4319805e7acf7fbdf2bac28a4b1a7370790a2a430c7adb1b29bb934eb",
42
+ "zh:998f27410a66158a35ee5ed142c27e5b21fe8601941da55da2157f8042d6dcca",
43
+ "zh:9b12af85486a96aedd8d7984b0ff811a4b42e3d88dad1a3fb4c0b580d04fa425",
44
+ "zh:9c1804eff1dda0446dc2d215231015bb65a2fc6c3b7ba24584fe45f1ddd3fa9f",
45
+ "zh:b03ff5efdee310502aaaeb460144dc059bce72a0d8217e6b989099ef8aef9283",
46
+ ]
47
+ }
@@ -0,0 +1,83 @@
1
+ """Retryable SES S3-action -> SNS (raw) -> SQS mailbox indexer.
2
+
3
+ Derived from markcallen/ses-receiving-terraform. Keep raw MIME immutable;
4
+ write deterministic per-envelope-recipient metadata only after S3 confirms it.
5
+ """
6
+ import json
7
+ import logging
8
+ import os
9
+ import re
10
+ from datetime import datetime
11
+
12
+ import boto3
13
+
14
+ s3 = boto3.client("s3")
15
+ logger = logging.getLogger(__name__)
16
+ logger.setLevel(logging.INFO)
17
+
18
+
19
+ def index_notification(payload):
20
+ bucket = os.environ["BUCKET"]
21
+ domain = os.environ["DOMAIN"].lower()
22
+ if payload.get("Type") == "Notification":
23
+ payload = json.loads(payload["Message"])
24
+ if payload.get("notificationType") != "Received":
25
+ raise ValueError("Expected an SES receiving notification")
26
+ mail, receipt = payload["mail"], payload["receipt"]
27
+ message_id = mail["messageId"]
28
+ if not isinstance(message_id, str) or not re.fullmatch(r"[a-zA-Z0-9_-]{1,200}", message_id):
29
+ raise ValueError("Invalid SES message ID")
30
+ raw_key = f"incoming/{message_id}"
31
+ action = receipt["action"]
32
+ if action.get("type") != "S3" or action.get("bucketName") != bucket or action.get("objectKey") not in (message_id, raw_key):
33
+ raise ValueError("Unexpected SES storage action")
34
+ received_at = mail["timestamp"]
35
+ timestamp = datetime.fromisoformat(received_at.replace("Z", "+00:00"))
36
+ if timestamp.tzinfo is None:
37
+ raise ValueError("Receipt timestamp must include a timezone")
38
+ # A failed read raises, leaving the SQS record available for retry.
39
+ head = s3.head_object(Bucket=bucket, Key=raw_key)
40
+ if head["ContentLength"] > 16 * 1024 * 1024:
41
+ raise ValueError("Message exceeds Hail's 16 MiB limit")
42
+ delivery = {
43
+ name: receipt[name]["status"]
44
+ for name in ("spfVerdict", "dkimVerdict", "dmarcVerdict", "spamVerdict", "virusVerdict")
45
+ if isinstance(receipt.get(name), dict) and isinstance(receipt[name].get("status"), str)
46
+ }
47
+ recipients = receipt["recipients"]
48
+ if not isinstance(recipients, list):
49
+ raise ValueError("Expected envelope recipients")
50
+ for recipient in sorted(set(value.lower() for value in recipients)):
51
+ local, separator, recipient_domain = recipient.partition("@")
52
+ if separator != "@" or recipient_domain != domain:
53
+ continue
54
+ if not re.fullmatch(r"[a-z0-9][a-z0-9._+-]{0,63}", local):
55
+ continue
56
+ metadata = {
57
+ "schemaVersion": 1,
58
+ "messageId": message_id,
59
+ "recipient": recipient,
60
+ "receivedAt": received_at,
61
+ "rawKey": raw_key,
62
+ "delivery": delivery,
63
+ }
64
+ # Same key and body on retries. Never delete raw mail or emit S3 events.
65
+ s3.put_object(
66
+ Bucket=bucket,
67
+ Key=f"index/{recipient}/{message_id}.json",
68
+ Body=json.dumps(metadata, sort_keys=True).encode("utf-8"),
69
+ ContentType="application/json",
70
+ )
71
+
72
+
73
+ def handler(event, context):
74
+ failures = []
75
+ for record in event["Records"]:
76
+ identifier = record["messageId"]
77
+ try:
78
+ index_notification(json.loads(record["body"]))
79
+ except Exception as error:
80
+ # Do not log message bodies, recipients, subjects, or token-bearing URLs.
81
+ logger.error("Mail indexing failed (%s)", type(error).__name__)
82
+ failures.append({"itemIdentifier": identifier})
83
+ return {"batchItemFailures": failures}