aboutsummaryrefslogtreecommitdiff
path: root/services/docker/mail-server/relay/aws/app.ts
diff options
context:
space:
mode:
Diffstat (limited to 'services/docker/mail-server/relay/aws/app.ts')
-rw-r--r--services/docker/mail-server/relay/aws/app.ts103
1 files changed, 103 insertions, 0 deletions
diff --git a/services/docker/mail-server/relay/aws/app.ts b/services/docker/mail-server/relay/aws/app.ts
index 51da795..76e2793 100644
--- a/services/docker/mail-server/relay/aws/app.ts
+++ b/services/docker/mail-server/relay/aws/app.ts
@@ -1,4 +1,5 @@
import { parseArgs } from "@std/cli";
+import { decodeBase64 } from "@std/encoding/base64";
import { z } from "zod";
import { zValidator } from "@hono/zod-validator";
@@ -9,6 +10,89 @@ import { AwsMailDeliverer } from "./deliver.ts";
import { AwsMailRetriever } from "./retriever.ts";
import config from "../config.ts";
+const AWS_SNS_MESSAGE_MODEL = z.object({
+ Type: z.enum(["Notification", "SubscriptionConfirmation"]),
+ TopicArn: z.string(),
+ Timestamp: z.string(),
+ Subject: z.string().optional(),
+ SubscribeURL: z.string().optional(),
+ Message: z.string(),
+ MessageId: z.string(),
+ Signature: z.string(),
+ SigningCertURL: z.string(),
+ SignatureVersion: z.string(),
+});
+
+type AwsSnsMessage = z.TypeOf<typeof AWS_SNS_MESSAGE_MODEL>;
+
+const AWS_SES_SNS_MESSAGE_MODEL = z.object({
+ notificationType: z.literal("Received").or(z.string()),
+ receipt: z.object({
+ recipients: z.array(z.string()),
+ }),
+ mail: z.object({
+ messageId: z.string(),
+ }),
+});
+
+const AWS_SNS_SIGNATURE_FIELDS = {
+ Notification: [
+ "Message",
+ "MessageId",
+ "Subject",
+ "Timestamp",
+ "TopicArn",
+ "Type",
+ ],
+ SubscriptionConfirmation: [
+ "Message",
+ "MessageId",
+ "SubscribeURL",
+ "Timestamp",
+ "TopicArn",
+ "Type",
+ ],
+} as const;
+
+async function verifySnsSignature(message: AwsSnsMessage) {
+ const signingCertUrl = message.SigningCertURL;
+
+ if (!new URL(signingCertUrl).hostname.endsWith(".amazonaws.com")) {
+ throw new Error(
+ `Signature cert url ${signingCertUrl} does not belong to aws!!!`,
+ );
+ }
+
+ const signature = message.Signature;
+ const data = AWS_SNS_SIGNATURE_FIELDS[message.Type].filter((field) =>
+ field in message
+ ).flatMap((field) => [field, message[field]]).join("\n");
+ const certData = await (await fetch(signingCertUrl)).bytes();
+ const key = await crypto.subtle.importKey(
+ "pkcs8",
+ certData,
+ {
+ name: "RSA-PSS",
+ hash: message.SignatureVersion === "1" ? "SHA-1" : "SHA-256",
+ },
+ false,
+ ["verify"],
+ );
+ const isVerified = await crypto.subtle.verify(
+ {
+ name: "RSA-PSS",
+ hash: message.SignatureVersion === "1" ? "SHA-1" : "SHA-256",
+ },
+ key,
+ decodeBase64(signature),
+ new TextEncoder().encode(data),
+ );
+
+ if (!isVerified) {
+ throw new Error("Signature does not match!!!");
+ }
+}
+
export class AwsRelayApp extends AppBase {
readonly #aws = new AwsContext();
readonly #retriever;
@@ -35,6 +119,25 @@ export class AwsRelayApp extends AppBase {
});
},
);
+ this.hono.post(
+ `/receive/aws-sns/${config.getValue("awsInboundPath")}`,
+ zValidator("json", AWS_SNS_MESSAGE_MODEL),
+ async (ctx) => {
+ const message = ctx.req.valid("json");
+ await verifySnsSignature(message);
+ if (message.Type === "Notification") {
+ const sesMessage = JSON.parse(message.Message);
+ const parsedSesMessage = AWS_SES_SNS_MESSAGE_MODEL.parse(sesMessage);
+ // TODO: Here!!! Specify receipts!
+ await this.#retriever.deliverS3Mail(parsedSesMessage.mail.messageId);
+ return ctx.json({
+ "msg": "Done!",
+ });
+ } else if (message.Type === "SubscriptionConfirmation") {
+ } else {
+ }
+ },
+ );
}
realServe() {