init app resources

This commit is contained in:
dswbx
2025-06-14 16:59:03 +02:00
parent 3338804c34
commit b87696a0db
13 changed files with 659 additions and 1 deletions

View File

@@ -0,0 +1,13 @@
export type TEmailResponse<Data = unknown> = {
success: boolean;
data?: Data;
};
export interface IEmailDriver<Data = unknown, Options = object> {
send(
to: string,
subject: string,
body: string | { text: string; html: string },
options?: Options,
): Promise<TEmailResponse<Data>>;
}

View File

@@ -0,0 +1,116 @@
import { mergeObject, type RecursivePartial } from "core/utils";
import type { IEmailDriver } from "./index";
export type MailchannelsEmailOptions = {
apiKey: string;
host?: string;
from?: { email: string; name: string };
};
export type Recipient = {
email: string;
name?: string;
};
export type MailchannelsSendOptions = RecursivePartial<{
attachments: Array<{
content: string;
filename: string;
type: string;
}>;
campaign_id: string;
content: Array<{
template_type?: string;
type: string;
value: string;
}>;
dkim_domain: string;
dkim_private_key: string;
dkim_selector: string;
from: Recipient;
headers: {};
personalizations: Array<{
bcc: Array<Recipient>;
cc: Array<Recipient>;
dkim_domain: string;
dkim_private_key: string;
dkim_selector: string;
dynamic_template_data: {};
from: Recipient;
headers: {};
reply_to: Recipient;
subject: string;
to: Array<Recipient>;
}>;
reply_to: Recipient;
subject: string;
tracking_settings: {
click_tracking: {
enable: boolean;
};
open_tracking: {
enable: boolean;
};
};
transactional: boolean;
}>;
export type MailchannelsEmailResponse = {
request_id: string;
results: Array<{
index: number;
message_id: string;
reason: string;
status: string;
}>;
};
export const mailchannelsEmail = (
config: MailchannelsEmailOptions,
): IEmailDriver<MailchannelsEmailResponse, MailchannelsSendOptions> => {
const host = config.host ?? "https://api.mailchannels.net/tx/v1/send";
const from = config.from ?? { email: "onboarding@mailchannels.net", name: "Mailchannels" };
return {
send: async (
to: string,
subject: string,
body: string | { text: string; html: string },
options?: MailchannelsSendOptions,
) => {
const payload: MailchannelsSendOptions = mergeObject(
{
from,
subject,
content:
typeof body === "string"
? [{ type: "text/html", value: body }]
: [
{ type: "text/plain", value: body.text },
{ type: "text/html", value: body.html },
],
personalizations: [
{
to: [{ email: to }],
},
],
},
options,
);
const res = await fetch(host, {
method: "POST",
headers: {
"Content-Type": "application/json",
"X-Api-Key": config.apiKey,
},
body: JSON.stringify({ ...payload, ...options }),
});
if (res.ok) {
const data = (await res.json()) as MailchannelsEmailResponse;
return { success: true, data };
}
return { success: false };
},
};
};

View File

@@ -0,0 +1,72 @@
import type { IEmailDriver } from "./index";
export type ResendEmailOptions = {
apiKey: string;
host?: string;
from?: string;
};
export type ResendEmailSendOptions = {
bcc?: string | string[];
cc?: string | string[];
reply_to?: string | string[];
scheduled_at?: string;
headers?: Record<string, string>;
attachments?: {
content: Buffer | string;
filename: string;
path: string;
content_type: string;
}[];
tags?: {
name: string;
value: string;
}[];
};
export type ResendEmailResponse = {
id: string;
};
export const resendEmail = (
config: ResendEmailOptions,
): IEmailDriver<ResendEmailResponse, ResendEmailSendOptions> => {
const host = config.host ?? "https://api.resend.com/emails";
const from = config.from ?? "Acme <onboarding@resend.dev>";
return {
send: async (
to: string,
subject: string,
body: string | { text: string; html: string },
options?: ResendEmailSendOptions,
) => {
const payload: any = {
from,
to,
subject,
};
if (typeof body === "string") {
payload.html = body;
} else {
payload.html = body.html;
payload.text = body.text;
}
const res = await fetch(host, {
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${config.apiKey}`,
},
body: JSON.stringify({ ...payload, ...options }),
});
if (res.ok) {
const data = (await res.json()) as ResendEmailResponse;
return { success: true, data };
}
return { success: false };
},
};
};

View File

@@ -0,0 +1,89 @@
import type { IEmailDriver } from "./index";
import { AwsClient } from "aws4fetch";
export type SesEmailOptions = {
region: string;
accessKeyId: string;
secretAccessKey: string;
from: string;
};
export type SesSendOptions = {
cc?: string[];
bcc?: string[];
replyTo?: string[];
};
export type SesEmailResponse = {
MessageId?: string;
status: number;
body: string;
};
export const sesEmail = (
config: SesEmailOptions,
): IEmailDriver<SesEmailResponse, SesSendOptions> => {
const endpoint = `https://email.${config.region}.amazonaws.com/`;
const from = config.from;
const aws = new AwsClient({
accessKeyId: config.accessKeyId,
secretAccessKey: config.secretAccessKey,
service: "ses",
region: config.region,
});
return {
send: async (
to: string,
subject: string,
body: string | { text: string; html: string },
options?: SesSendOptions,
) => {
// build SES SendEmail params (x-www-form-urlencoded)
const params: Record<string, string> = {
Action: "SendEmail",
Version: "2010-12-01",
Source: from,
"Destination.ToAddresses.member.1": to,
"Message.Subject.Data": subject,
};
if (typeof body === "string") {
params["Message.Body.Html.Data"] = body;
} else {
params["Message.Body.Html.Data"] = body.html;
params["Message.Body.Text.Data"] = body.text;
}
if (options?.cc) {
options.cc.forEach((cc, i) => {
params[`Destination.CcAddresses.member.${i + 1}`] = cc;
});
}
if (options?.bcc) {
options.bcc.forEach((bcc, i) => {
params[`Destination.BccAddresses.member.${i + 1}`] = bcc;
});
}
if (options?.replyTo) {
options.replyTo.forEach((reply, i) => {
params[`ReplyToAddresses.member.${i + 1}`] = reply;
});
}
const formBody = Object.entries(params)
.map(([k, v]) => encodeURIComponent(k) + "=" + encodeURIComponent(v))
.join("&");
const res = await aws.fetch(endpoint, {
method: "POST",
headers: { "content-type": "application/x-www-form-urlencoded" },
body: formBody,
});
const text = await res.text();
// try to extract MessageId from XML response
let MessageId: string | undefined = undefined;
const match = text.match(/<MessageId>([^<]+)<\/MessageId>/);
if (match) MessageId = match[1];
return {
success: res.ok,
data: { MessageId, status: res.status, body: text },
};
},
};
};