Skip to content

Commit fc2018a

Browse files
committed
Remove resend, use Postmark
Resend has been plagued with security issues lately, and I can no longer recommend using them for production apps, especially ones which send authentication links via email. Postmark has been a leader in the space for a long time and is one of the easiest and most reliable email services to use.
1 parent 92c68c1 commit fc2018a

File tree

11 files changed

+120
-26
lines changed

11 files changed

+120
-26
lines changed

.env

+2-2
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,6 @@ AUTH_GOOGLE_SECRET=
3131
AUTH_GITHUB_ID=
3232
AUTH_GITHUB_SECRET=
3333

34-
# Email (https://resend.com)
34+
# Email (https://postmarkapp.com)
3535
EMAIL_FROM="StartKit <[email protected]>"
36-
RESEND_API_KEY=
36+
POSTMARK_API_KEY=

README.md

+2-2
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@
1818
- [TailwindCSS](https://tailwindcss.com/) for utility-first CSS.
1919
- Gorgeous UI built with [Radix](https://www.radix-ui.com/) and [shadcn/ui](https://ui.shadcn.com/).
2020
- Authentication via [Next Auth](https://next-auth.js.org/) version 5.
21-
- Email via [Resend](https://resend.com) and [react email](https://react.email/).
21+
- Email via [Postmark](https://postmarkapp.com) and [jsx-email](https://jsx.email/).
2222
- The beautiful [Geist](https://vercel.com/font) typeface.
2323
- [Next Metadata API](https://beta.nextjs.org/docs/api-reference/metadata) for SEO handling, with file-system handlers.
2424
- [Jest](https://jestjs.io/) testing, optimized for Next.js
@@ -125,7 +125,7 @@ bun run db studio
125125

126126
## Email
127127

128-
Email is currently configured to send via [Resend](https://resend.com), and uses the wonderful [jsx-email](https://jsx.email) library.
128+
Email is configured to send via the amazing [Postmark](https://postmarkapp.com) email service, and uses the wonderful [jsx-email](https://jsx.email) library.
129129

130130
Email templates live with your react code and are defined in [`./emails`](./emails).
131131

app/(marketing)/features.tsx

+1-1
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ export function Features() {
2929
{ title: "TailwindCSS", href: "https://tailwindcss.com" },
3030
{ title: "Radix UI", href: "https://www.radix-ui.com" },
3131
{ title: "PostgreSQL" },
32-
{ title: "Email via Resend", href: "https://resend.com" },
32+
{ title: "Email via Postmark", href: "https://postmarkapp.com" },
3333
{ title: "Vercel ready", href: "https://vercel.com" },
3434
{ title: "Metadata SEO" },
3535
{ title: "Geist Font", href: "https://vercel.com/font" },

bun.lockb

-825 Bytes
Binary file not shown.

env/env.mjs

+1-1
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ export const env = createEnv({
3939
* Email
4040
*/
4141
EMAIL_FROM: z.string(),
42-
RESEND_API_KEY: z.string().optional()
42+
POSTMARK_API_KEY: z.string().default("")
4343
},
4444
/**
4545
* Shared between server and client

lib/auth/send-verification-request.tsx

+5-5
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import { usersTable } from "@/drizzle/schema"
66
import { Template as SignInEmail } from "@/emails/signin-email"
77
import { env } from "@/env"
88
import { db } from "@/lib/db"
9-
import { emailClient } from "@/lib/email"
9+
import { sendEmail } from "@/lib/email/send-email"
1010

1111
type SendVerificationRequestParams = Parameters<
1212
EmailConfig["sendVerificationRequest"]
@@ -33,12 +33,12 @@ export async function sendVerificationRequest({
3333
url={url}
3434
/>
3535
)
36-
const html = await render(template)
37-
const text = await render(template, { plainText: true })
36+
const html = await render(template, { minify: false })
37+
const text = await render(template, { plainText: true, minify: false })
3838

39-
await emailClient().emails.send({
40-
from: env.EMAIL_FROM,
39+
await sendEmail({
4140
to: email,
41+
from: env.EMAIL_FROM,
4242
headers: {
4343
"X-Entity-Ref-ID": Date.now().toString()
4444
},

lib/email/client.ts

-13
This file was deleted.

lib/email/index.ts

-1
This file was deleted.

lib/email/send-email.ts

+53
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
import { env } from "@/env"
2+
3+
export type SendEmailParams = {
4+
to: string
5+
from: string
6+
subject: string
7+
html?: string
8+
text?: string
9+
headers?: Record<string, string>
10+
}
11+
12+
/**
13+
* Sends an email using Postmark's API.
14+
*
15+
* @see {@link https://postmarkapp.com/developer/user-guide/send-email-with-api}
16+
*/
17+
export async function sendEmail({
18+
to,
19+
from,
20+
subject,
21+
html,
22+
text,
23+
headers
24+
}: SendEmailParams) {
25+
const response = await fetch("https://api.postmarkapp.com/email", {
26+
method: "POST",
27+
headers: {
28+
Accept: "application/json",
29+
"Content-Type": "application/json",
30+
"X-Postmark-Server-Token": env.POSTMARK_API_KEY
31+
},
32+
body: JSON.stringify({
33+
From: from,
34+
To: to,
35+
Subject: subject,
36+
HtmlBody: html,
37+
TextBody: text,
38+
MessageStream: "outbound",
39+
Headers:
40+
headers &&
41+
Object.entries(headers).map(([key, value]) => ({
42+
Name: key,
43+
Value: value
44+
}))
45+
})
46+
})
47+
48+
if (!response.ok) {
49+
throw new Error(`Email send failed: ${response.status}`)
50+
}
51+
52+
return response.json()
53+
}

package.json

-1
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,6 @@
5050
"react": "^18.2.0",
5151
"react-dom": "^18.2.0",
5252
"react-hook-form": "^7.50.1",
53-
"resend": "^3.2.0",
5453
"sonner": "^1.4.1",
5554
"tailwind-merge": "^2.2.1",
5655
"typed-route-handler": "^0.1.1",

patches/jsx-email+1.10.11.patch

+56
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
diff --git a/node_modules/jsx-email/dist/index.js b/node_modules/jsx-email/dist/index.js
2+
index 27b1de3..d28bf47 100644
3+
--- a/node_modules/jsx-email/dist/index.js
4+
+++ b/node_modules/jsx-email/dist/index.js
5+
@@ -1040,7 +1040,7 @@ Text.displayName = "Text";
6+
var import_html_to_text = require("html-to-text");
7+
8+
// src/render/process.ts
9+
-var import_minify_preset = require("@jsx-email/minify-preset");
10+
+// var import_minify_preset = require("@jsx-email/minify-preset");
11+
var import_pretty = __toESM(require("pretty"));
12+
var jsxEmailTags = ["jsx-email-cond"];
13+
var processHtml = /* @__PURE__ */ __name(async ({ html, minify, pretty }) => {
14+
@@ -1075,10 +1075,10 @@ var processHtml = /* @__PURE__ */ __name(async ({ html, minify, pretty }) => {
15+
}
16+
__name(rehypeMoveStyle, "rehypeMoveStyle");
17+
let processor = rehype().data("settings", settings).use(rehypeMoveStyle);
18+
- if (minify) {
19+
- const preset = await (0, import_minify_preset.minifyPreset)();
20+
- processor = processor.use(preset);
21+
- }
22+
+ // if (minify) {
23+
+ // const preset = await (0, import_minify_preset.minifyPreset)();
24+
+ // processor = processor.use(preset);
25+
+ // }
26+
const doc = await processor.use(stringify, {
27+
allowDangerousCharacters: true,
28+
allowDangerousHtml: true,
29+
diff --git a/node_modules/jsx-email/dist/index.mjs b/node_modules/jsx-email/dist/index.mjs
30+
index 90b43c1..15bffd5 100644
31+
--- a/node_modules/jsx-email/dist/index.mjs
32+
+++ b/node_modules/jsx-email/dist/index.mjs
33+
@@ -976,7 +976,7 @@ Text.displayName = "Text";
34+
import { htmlToText } from "html-to-text";
35+
36+
// src/render/process.ts
37+
-import { minifyPreset } from "@jsx-email/minify-preset";
38+
+// import { minifyPreset } from "@jsx-email/minify-preset";
39+
import prettyHtml from "pretty";
40+
var jsxEmailTags = ["jsx-email-cond"];
41+
var processHtml = /* @__PURE__ */ __name(async ({ html, minify, pretty }) => {
42+
@@ -1011,10 +1011,10 @@ var processHtml = /* @__PURE__ */ __name(async ({ html, minify, pretty }) => {
43+
}
44+
__name(rehypeMoveStyle, "rehypeMoveStyle");
45+
let processor = rehype().data("settings", settings).use(rehypeMoveStyle);
46+
- if (minify) {
47+
- const preset = await minifyPreset();
48+
- processor = processor.use(preset);
49+
- }
50+
+ // if (minify) {
51+
+ // const preset = await minifyPreset();
52+
+ // processor = processor.use(preset);
53+
+ // }
54+
const doc = await processor.use(stringify, {
55+
allowDangerousCharacters: true,
56+
allowDangerousHtml: true,

0 commit comments

Comments
 (0)