Add attachment support

This commit is contained in:
KMKoushik
2024-04-19 20:58:30 +10:00
parent 9465960f0a
commit 80878679cd
22 changed files with 1029 additions and 190 deletions

View File

@@ -1,24 +1,55 @@
import { EmailStatus } from "@prisma/client";
import { z } from "zod";
import {
createTRPCRouter,
protectedProcedure,
publicProcedure,
teamProcedure,
} from "~/server/api/trpc";
import { createTRPCRouter, teamProcedure } from "~/server/api/trpc";
import { db } from "~/server/db";
import { createDomain, getDomain } from "~/server/service/domain-service";
const statuses = Object.values(EmailStatus) as [EmailStatus];
const DEFAULT_LIMIT = 30;
export const emailRouter = createTRPCRouter({
emails: teamProcedure.query(async ({ ctx }) => {
const emails = await db.email.findMany({
where: {
teamId: ctx.team.id,
},
});
emails: teamProcedure
.input(
z.object({
page: z.number().optional(),
status: z.enum(statuses).optional().nullable(),
domain: z.number().optional(),
})
)
.query(async ({ ctx, input }) => {
const page = input.page || 1;
const limit = DEFAULT_LIMIT;
const offset = (page - 1) * limit;
return emails;
}),
const whereConditions = {
teamId: ctx.team.id,
...(input.status ? { latestStatus: input.status } : {}),
...(input.domain ? { domainId: input.domain } : {}),
};
const countP = db.email.count({ where: whereConditions });
const emailsP = db.email.findMany({
where: whereConditions,
select: {
id: true,
createdAt: true,
latestStatus: true,
subject: true,
to: true,
},
orderBy: {
createdAt: "desc",
},
skip: offset,
take: limit,
});
const [emails, count] = await Promise.all([emailsP, countP]);
return { emails, totalPage: Math.ceil(count / limit) };
}),
getEmail: teamProcedure
.input(z.object({ id: z.string() }))
@@ -30,7 +61,7 @@ export const emailRouter = createTRPCRouter({
include: {
emailEvents: {
orderBy: {
createdAt: "desc",
createdAt: "asc",
},
},
},

View File

@@ -10,6 +10,7 @@ import {
EventType,
} from "@aws-sdk/client-sesv2";
import { generateKeyPairSync } from "crypto";
import mime from "mime-types";
import { env } from "~/env";
import { EmailContent } from "~/types";
import { APP_SETTINGS } from "~/utils/constants";
@@ -154,6 +155,63 @@ export async function sendEmailThroughSes({
}
}
export async function sendEmailWithAttachments({
to,
from,
subject,
text,
html,
attachments,
region = "us-east-1",
configurationSetName,
}: EmailContent & {
region?: string;
configurationSetName: string;
attachments: { filename: string; content: string }[];
}) {
const sesClient = getSesClient(region);
const boundary = "NextPart";
let rawEmail = `From: ${from}\n`;
rawEmail += `To: ${to}\n`;
rawEmail += `Subject: ${subject}\n`;
rawEmail += `MIME-Version: 1.0\n`;
rawEmail += `Content-Type: multipart/mixed; boundary="${boundary}"\n\n`;
rawEmail += `--${boundary}\n`;
rawEmail += `Content-Type: text/html; charset="UTF-8"\n\n`;
rawEmail += `${html}\n\n`;
for (const attachment of attachments) {
const content = attachment.content; // Convert buffer to base64
const mimeType =
mime.lookup(attachment.filename) || "application/octet-stream";
rawEmail += `--${boundary}\n`;
rawEmail += `Content-Type: ${mimeType}; name="${attachment.filename}"\n`;
rawEmail += `Content-Disposition: attachment; filename="${attachment.filename}"\n`;
rawEmail += `Content-Transfer-Encoding: base64\n\n`;
rawEmail += `${content}\n\n`;
}
rawEmail += `--${boundary}--`;
const command = new SendEmailCommand({
Content: {
Raw: {
Data: Buffer.from(rawEmail),
},
},
ConfigurationSetName: configurationSetName,
});
try {
const response = await sesClient.send(command);
console.log("Email with attachments sent! Message ID:", response.MessageId);
return response.MessageId;
} catch (error) {
console.error("Failed to send email with attachments", error);
throw new Error("Failed to send email with attachments");
}
}
export async function addWebhookConfiguration(
configName: string,
topicArn: string,

View File

@@ -19,6 +19,14 @@ const route = createRoute({
subject: z.string(),
text: z.string().optional(),
html: z.string().optional(),
attachments: z
.array(
z.object({
filename: z.string(),
content: z.string(),
})
)
.optional(),
}),
},
},

View File

@@ -1,12 +1,12 @@
import { EmailContent } from "~/types";
import { db } from "../db";
import { sendEmailThroughSes } from "../aws/ses";
import { sendEmailThroughSes, sendEmailWithAttachments } from "../aws/ses";
import { APP_SETTINGS } from "~/utils/constants";
export async function sendEmail(
emailContent: EmailContent & { teamId: number }
) {
const { to, from, subject, text, html, teamId } = emailContent;
const { to, from, subject, text, html, teamId, attachments } = emailContent;
const fromDomain = from.split("@")[1];
@@ -24,18 +24,33 @@ export async function sendEmail(
throw new Error("Domain is not verified");
}
const messageId = await sendEmailThroughSes({
to,
from,
subject,
text,
html,
region: domain.region,
configurationSetName: getConfigurationSetName(
domain.clickTracking,
domain.openTracking
),
});
const messageId = attachments
? await sendEmailWithAttachments({
to,
from,
subject,
text,
html,
region: domain.region,
configurationSetName: getConfigurationSetName(
domain.clickTracking,
domain.openTracking
),
attachments,
})
: await sendEmailThroughSes({
to,
from,
subject,
text,
html,
region: domain.region,
configurationSetName: getConfigurationSetName(
domain.clickTracking,
domain.openTracking
),
attachments,
});
if (messageId) {
return await db.email.create({