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:
@@ -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);
|
||||
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user