Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 11 additions & 3 deletions example/convex/_generated/api.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@ export declare const components: {
headers?: Array<{ name: string; value: string }>;
replyTo?: Array<string>;
subject: string;
to: string;
to: Array<string> | string;
},
string
>;
Expand All @@ -80,9 +80,15 @@ export declare const components: {
"internal",
{ emailId: string },
{
bcc?: Array<string>;
bounceCount?: number;
cc?: Array<string>;
complained: boolean;
complaintCount?: number;
createdAt: number;
deliveryDelayedCount?: number;
errorMessage?: string;
failedCount?: number;
finalizedAt: number;
from: string;
headers?: Array<{ name: string; value: string }>;
Expand All @@ -102,7 +108,7 @@ export declare const components: {
| "failed";
subject: string;
text?: string;
to: string;
to: Array<string>;
} | null
>;
getStatus: FunctionReference<
Expand Down Expand Up @@ -134,6 +140,8 @@ export declare const components: {
"mutation",
"internal",
{
bcc?: Array<string>;
cc?: Array<string>;
from: string;
headers?: Array<{ name: string; value: string }>;
html?: string;
Expand All @@ -147,7 +155,7 @@ export declare const components: {
replyTo?: Array<string>;
subject: string;
text?: string;
to: string;
to: Array<string>;
},
string
>;
Expand Down
19 changes: 17 additions & 2 deletions example/convex/example.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,8 +49,23 @@ export const sendOne = internalAction({
args: { to: v.optional(v.string()) },
handler: async (ctx, args) => {
const email = await resend.sendEmail(ctx, {
from: "<your-verified-sender-address>",
to: args.to ?? "[email protected]",
from: "[email protected]",
to: args.to ?? [
"[email protected]",
"[email protected]",
"[email protected]",
"[email protected]",
"[email protected]",
"[email protected]",
"[email protected]",
"[email protected]",
"[email protected]",
"[email protected]",
"[email protected]",
"[email protected]",
"[email protected]",
"[email protected]",
],
subject: "Test Email",
html: "This is a test email",
});
Expand Down
19 changes: 16 additions & 3 deletions src/client/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -144,7 +144,9 @@ export type EmailStatus = {

export type SendEmailOptions = {
from: string;
to: string;
to: string | string[];
cc?: string | string[];
bcc?: string | string[];
subject: string;
html?: string;
text?: string;
Expand Down Expand Up @@ -247,7 +249,7 @@ export class Resend {
replyTo?: string[],
headers?: { name: string; value: string }[]
) {
const sendEmailArgs =
const sendEmailArgs: SendEmailOptions =
typeof fromOrOptions === "string"
? {
from: fromOrOptions,
Expand All @@ -265,6 +267,12 @@ export class Resend {
const id = await ctx.runMutation(this.component.lib.sendEmail, {
options: await configToRuntimeConfig(this.config, this.onEmailEvent),
...sendEmailArgs,
to:
typeof sendEmailArgs.to === "string"
? [sendEmailArgs.to]
: sendEmailArgs.to,
cc: toArray(sendEmailArgs.cc),
bcc: toArray(sendEmailArgs.bcc),
});

return id as EmailId;
Expand Down Expand Up @@ -355,7 +363,7 @@ export class Resend {
emailId: EmailId
): Promise<{
from: string;
to: string;
to: string[];
subject: string;
replyTo: string[];
headers?: { name: string; value: string }[];
Expand Down Expand Up @@ -466,3 +474,8 @@ export type UseApi<API> = Expand<{
>
: UseApi<API[mod]>;
}>;

function toArray<T>(value: T | T[] | undefined): T[] | undefined {
if (value === undefined) return undefined;
return Array.isArray(value) ? value : [value];
}
31 changes: 31 additions & 0 deletions src/component/lib.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,16 @@ describe("handleEmailEvent", () => {
expect(updatedEmail.status).toBe("delivered");
expect(updatedEmail.finalizedAt).toBeLessThan(Number.MAX_SAFE_INTEGER);
expect(updatedEmail.finalizedAt).toBeGreaterThan(Date.now() - 10000); // Within last 10 seconds
// deliveryEvents entry created
const events = await t.run(async (ctx) =>
ctx.db
.query("deliveryEvents")
.withIndex("by_emailId", (q) => q.eq("emailId", email._id))
.collect()
);
expect(events.length).toBe(1);
expect(events[0].eventType).toBe("email.delivered");
expect(events[0].resendId).toBe("test-resend-id-123");
});

it("updates email for complained event", async () => {
Expand All @@ -52,6 +62,15 @@ describe("handleEmailEvent", () => {
const updatedEmail = await getEmail();
expect(updatedEmail.status).toBe("sent");
expect(updatedEmail.complained).toBe(true);
// deliveryEvents entry created
const events = await t.run(async (ctx) =>
ctx.db
.query("deliveryEvents")
.withIndex("by_emailId", (q) => q.eq("emailId", email._id))
.collect()
);
expect(events.length).toBe(1);
expect(events[0].eventType).toBe("email.complained");
});

it("updates email for bounced event", async () => {
Expand All @@ -67,6 +86,18 @@ describe("handleEmailEvent", () => {
expect(updatedEmail.errorMessage).toBe(
"The email bounced due to invalid recipient"
);
// deliveryEvents entry created
const events = await t.run(async (ctx) =>
ctx.db
.query("deliveryEvents")
.withIndex("by_emailId", (q) => q.eq("emailId", email._id))
.collect()
);
expect(events.length).toBe(1);
expect(events[0].eventType).toBe("email.bounced");
expect(events[0].message).toBe(
"The email bounced due to invalid recipient"
);
});

it("updates email for delivery_delayed event", async () => {
Expand Down
Loading