Improve Self host setup (#30)

* Add self host setup

* Improve blunders

* Move to bull mq

* More changes

* Add example code for sending test emails
This commit is contained in:
KM Koushik
2024-06-24 08:21:37 +10:00
committed by GitHub
parent 8a2769621c
commit f77a8829be
67 changed files with 1771 additions and 688 deletions
+39 -17
View File
@@ -8,14 +8,14 @@ import {
CreateConfigurationSetEventDestinationCommand,
CreateConfigurationSetCommand,
EventType,
GetAccountCommand,
} 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";
function getSesClient(region = "us-east-1") {
function getSesClient(region: string) {
return new SESv2Client({
region: region,
credentials: {
@@ -51,7 +51,7 @@ function generateKeyPair() {
return { privateKey: base64PrivateKey, publicKey: base64PublicKey };
}
export async function addDomain(domain: string, region = "us-east-1") {
export async function addDomain(domain: string, region: string) {
const sesClient = getSesClient(region);
const { privateKey, publicKey } = generateKeyPair();
@@ -61,7 +61,6 @@ export async function addDomain(domain: string, region = "us-east-1") {
DomainSigningSelector: "unsend",
DomainSigningPrivateKey: privateKey,
},
ConfigurationSetName: APP_SETTINGS.SES_CONFIGURATION_GENERAL,
});
const response = await sesClient.send(command);
@@ -84,7 +83,7 @@ export async function addDomain(domain: string, region = "us-east-1") {
return publicKey;
}
export async function deleteDomain(domain: string, region = "us-east-1") {
export async function deleteDomain(domain: string, region: string) {
const sesClient = getSesClient(region);
const command = new DeleteEmailIdentityCommand({
EmailIdentity: domain,
@@ -93,7 +92,7 @@ export async function deleteDomain(domain: string, region = "us-east-1") {
return response.$metadata.httpStatusCode === 200;
}
export async function getDomainIdentity(domain: string, region = "us-east-1") {
export async function getDomainIdentity(domain: string, region: string) {
const sesClient = getSesClient(region);
const command = new GetEmailIdentityCommand({
EmailIdentity: domain,
@@ -106,21 +105,29 @@ export async function sendEmailThroughSes({
to,
from,
subject,
cc,
bcc,
text,
html,
replyTo,
region = "us-east-1",
region,
configurationSetName,
}: EmailContent & {
region?: string;
}: Partial<EmailContent> & {
region: string;
configurationSetName: string;
cc?: string[];
bcc?: string[];
replyTo?: string[];
to?: string[];
}) {
const sesClient = getSesClient(region);
const command = new SendEmailCommand({
FromEmailAddress: from,
ReplyToAddresses: replyTo ? [replyTo] : undefined,
ReplyToAddresses: replyTo ? replyTo : undefined,
Destination: {
ToAddresses: [to],
ToAddresses: to,
CcAddresses: cc,
BccAddresses: bcc,
},
Content: {
// EmailContent
@@ -153,7 +160,7 @@ export async function sendEmailThroughSes({
return response.MessageId;
} catch (error) {
console.error("Failed to send email", error);
throw new Error("Failed to send email");
throw error;
}
}
@@ -163,21 +170,29 @@ export async function sendEmailWithAttachments({
from,
subject,
replyTo,
cc,
bcc,
// eslint-disable-next-line no-unused-vars
text,
html,
attachments,
region = "us-east-1",
region,
configurationSetName,
}: EmailContent & {
region?: string;
}: Partial<EmailContent> & {
region: string;
configurationSetName: string;
attachments: { filename: string; content: string }[];
cc?: string[];
bcc?: string[];
replyTo?: string[];
to?: string[];
}) {
const sesClient = getSesClient(region);
const boundary = "NextPart";
let rawEmail = `From: ${from}\n`;
rawEmail += `To: ${to}\n`;
rawEmail += `To: ${Array.isArray(to) ? to.join(", ") : to}\n`;
rawEmail += `Cc: ${cc ? cc.join(", ") : ""}\n`;
rawEmail += `Bcc: ${bcc ? bcc.join(", ") : ""}\n`;
rawEmail += `Reply-To: ${replyTo}\n`;
rawEmail += `Subject: ${subject}\n`;
rawEmail += `MIME-Version: 1.0\n`;
@@ -217,11 +232,18 @@ export async function sendEmailWithAttachments({
}
}
export async function getAccount(region: string) {
const client = getSesClient(region);
const input = new GetAccountCommand({});
const response = await client.send(input);
return response;
}
export async function addWebhookConfiguration(
configName: string,
topicArn: string,
eventTypes: EventType[],
region = "us-east-1"
region: string
) {
const sesClient = getSesClient(region);
-99
View File
@@ -1,99 +0,0 @@
import { APP_SETTINGS } from "~/utils/constants";
import { createTopic, subscribeEndpoint } from "./sns";
import { env } from "~/env";
import { AppSettingsService } from "~/server/service/app-settings-service";
import { addWebhookConfiguration } from "./ses";
import { EventType } from "@aws-sdk/client-sesv2";
const GENERAL_EVENTS: EventType[] = [
"BOUNCE",
"COMPLAINT",
"DELIVERY",
"DELIVERY_DELAY",
"REJECT",
"RENDERING_FAILURE",
"SEND",
"SUBSCRIPTION",
];
export async function setupAws() {
AppSettingsService.initializeCache();
let snsTopicArn = await AppSettingsService.getSetting(
APP_SETTINGS.SNS_TOPIC_ARN
);
console.log("Setting up AWS");
if (!snsTopicArn) {
console.log("SNS topic not present, creating...");
snsTopicArn = await createUnsendSNSTopic();
}
await setupSESConfiguration();
}
async function createUnsendSNSTopic() {
const topicArn = await createTopic(env.SNS_TOPIC);
if (!topicArn) {
console.error("Failed to create SNS topic");
return;
}
await subscribeEndpoint(
topicArn,
`${env.APP_URL ?? env.NEXTAUTH_URL}/api/ses_callback`
);
return await AppSettingsService.setSetting(
APP_SETTINGS.SNS_TOPIC_ARN,
topicArn
);
}
async function setupSESConfiguration() {
const topicArn = (
await AppSettingsService.getSetting(APP_SETTINGS.SNS_TOPIC_ARN)
)?.toString();
if (!topicArn) {
return;
}
console.log("Setting up SES webhook configuration");
await setWebhookConfiguration(
APP_SETTINGS.SES_CONFIGURATION_GENERAL,
topicArn,
GENERAL_EVENTS
);
await setWebhookConfiguration(
APP_SETTINGS.SES_CONFIGURATION_CLICK_TRACKING,
topicArn,
[...GENERAL_EVENTS, "CLICK"]
);
await setWebhookConfiguration(
APP_SETTINGS.SES_CONFIGURATION_OPEN_TRACKING,
topicArn,
[...GENERAL_EVENTS, "OPEN"]
);
await setWebhookConfiguration(APP_SETTINGS.SES_CONFIGURATION_FULL, topicArn, [
...GENERAL_EVENTS,
"CLICK",
"OPEN",
]);
}
async function setWebhookConfiguration(
setting: string,
topicArn: string,
eventTypes: EventType[]
) {
const sesConfigurationGeneral = await AppSettingsService.getSetting(setting);
if (!sesConfigurationGeneral) {
console.log(`Setting up SES webhook configuration for ${setting}`);
const status = await addWebhookConfiguration(setting, topicArn, eventTypes);
await AppSettingsService.setSetting(setting, status.toString());
}
}
+9 -5
View File
@@ -5,7 +5,7 @@ import {
} from "@aws-sdk/client-sns";
import { env } from "~/env";
function getSnsClient(region = "us-east-1") {
function getSnsClient(region: string) {
return new SNSClient({
region: region,
credentials: {
@@ -15,8 +15,8 @@ function getSnsClient(region = "us-east-1") {
});
}
export async function createTopic(topic: string) {
const client = getSnsClient();
export async function createTopic(topic: string, region: string) {
const client = getSnsClient(region);
const command = new CreateTopicCommand({
Name: topic,
});
@@ -25,13 +25,17 @@ export async function createTopic(topic: string) {
return data.TopicArn;
}
export async function subscribeEndpoint(topicArn: string, endpointUrl: string) {
export async function subscribeEndpoint(
topicArn: string,
endpointUrl: string,
region: string
) {
const subscribeCommand = new SubscribeCommand({
Protocol: "https",
TopicArn: topicArn,
Endpoint: endpointUrl,
});
const client = getSnsClient();
const client = getSnsClient(region);
const data = await client.send(subscribeCommand);
console.log(data.SubscriptionArn);