fix: use nodemailer to compose email (#166)
This commit is contained in:
@@ -48,6 +48,7 @@
|
|||||||
"nanoid": "^5.1.5",
|
"nanoid": "^5.1.5",
|
||||||
"next": "^15.3.1",
|
"next": "^15.3.1",
|
||||||
"next-auth": "^4.24.11",
|
"next-auth": "^4.24.11",
|
||||||
|
"nodemailer": "^7.0.3",
|
||||||
"pnpm": "^10.9.0",
|
"pnpm": "^10.9.0",
|
||||||
"prisma": "^6.6.0",
|
"prisma": "^6.6.0",
|
||||||
"query-string": "^9.1.1",
|
"query-string": "^9.1.1",
|
||||||
@@ -70,6 +71,7 @@
|
|||||||
"@types/html-to-text": "^9.0.4",
|
"@types/html-to-text": "^9.0.4",
|
||||||
"@types/mime-types": "^2.1.4",
|
"@types/mime-types": "^2.1.4",
|
||||||
"@types/node": "^22.15.2",
|
"@types/node": "^22.15.2",
|
||||||
|
"@types/nodemailer": "^6.4.17",
|
||||||
"@types/react": "^19.1.2",
|
"@types/react": "^19.1.2",
|
||||||
"@types/react-dom": "^19.1.2",
|
"@types/react-dom": "^19.1.2",
|
||||||
"@types/ua-parser-js": "^0.7.39",
|
"@types/ua-parser-js": "^0.7.39",
|
||||||
|
@@ -12,6 +12,8 @@ import {
|
|||||||
} from "@aws-sdk/client-sesv2";
|
} from "@aws-sdk/client-sesv2";
|
||||||
import { generateKeyPairSync } from "crypto";
|
import { generateKeyPairSync } from "crypto";
|
||||||
import mime from "mime-types";
|
import mime from "mime-types";
|
||||||
|
import nodemailer from "nodemailer";
|
||||||
|
import { Readable } from "stream";
|
||||||
import { env } from "~/env";
|
import { env } from "~/env";
|
||||||
import { EmailContent } from "~/types";
|
import { EmailContent } from "~/types";
|
||||||
import { nanoid } from "../nanoid";
|
import { nanoid } from "../nanoid";
|
||||||
@@ -110,8 +112,7 @@ export async function sendRawEmail({
|
|||||||
replyTo,
|
replyTo,
|
||||||
cc,
|
cc,
|
||||||
bcc,
|
bcc,
|
||||||
// eslint-disable-next-line no-unused-vars
|
text,
|
||||||
text, // text is not used directly in raw email but kept for interface consistency
|
|
||||||
html,
|
html,
|
||||||
attachments,
|
attachments,
|
||||||
region,
|
region,
|
||||||
@@ -132,54 +133,51 @@ export async function sendRawEmail({
|
|||||||
inReplyToMessageId?: string;
|
inReplyToMessageId?: string;
|
||||||
}) {
|
}) {
|
||||||
const sesClient = getSesClient(region);
|
const sesClient = getSesClient(region);
|
||||||
const boundary = `NextPart`;
|
|
||||||
let rawEmail = `From: ${from}\n`;
|
|
||||||
rawEmail += `To: ${Array.isArray(to) ? to.join(", ") : to}\n`;
|
|
||||||
rawEmail += cc && cc.length ? `Cc: ${cc.join(", ")}\n` : "";
|
|
||||||
rawEmail += bcc && bcc.length ? `Bcc: ${bcc.join(", ")}\n` : "";
|
|
||||||
rawEmail +=
|
|
||||||
replyTo && replyTo.length ? `Reply-To: ${replyTo.join(", ")}\n` : "";
|
|
||||||
rawEmail += `Subject: ${subject}\n`;
|
|
||||||
rawEmail += `MIME-Version: 1.0\n`;
|
|
||||||
|
|
||||||
// Add headers
|
const { message: messageStream } = await nodemailer
|
||||||
if (unsubUrl) {
|
.createTransport({ streamTransport: true })
|
||||||
rawEmail += `List-Unsubscribe: <${unsubUrl}>\n`;
|
.sendMail({
|
||||||
rawEmail += `List-Unsubscribe-Post: List-Unsubscribe=One-Click\n`;
|
from,
|
||||||
}
|
to,
|
||||||
if (isBulk) {
|
subject,
|
||||||
rawEmail += `Precedence: bulk\n`;
|
html,
|
||||||
}
|
attachments: attachments?.map((attachment) => ({
|
||||||
if (inReplyToMessageId) {
|
filename: attachment.filename,
|
||||||
rawEmail += `In-Reply-To: <${inReplyToMessageId}@email.amazonses.com>\n`;
|
content: attachment.content,
|
||||||
rawEmail += `References: <${inReplyToMessageId}@email.amazonses.com>\n`;
|
encoding: "base64",
|
||||||
}
|
})),
|
||||||
rawEmail += `X-Entity-Ref-ID: ${nanoid()}\n`;
|
text,
|
||||||
|
replyTo,
|
||||||
|
cc,
|
||||||
|
bcc,
|
||||||
|
headers: {
|
||||||
|
"X-Entity-Ref-ID": nanoid(),
|
||||||
|
...(unsubUrl
|
||||||
|
? {
|
||||||
|
"List-Unsubscribe": `<${unsubUrl}>`,
|
||||||
|
"List-Unsubscribe-Post": "List-Unsubscribe=One-Click",
|
||||||
|
}
|
||||||
|
: {}),
|
||||||
|
...(isBulk ? { Precedence: "bulk" } : {}),
|
||||||
|
...(inReplyToMessageId
|
||||||
|
? {
|
||||||
|
"In-Reply-To": `<${inReplyToMessageId}@email.amazonses.com>`,
|
||||||
|
References: `<${inReplyToMessageId}@email.amazonses.com>`,
|
||||||
|
}
|
||||||
|
: {}),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
rawEmail += `Content-Type: multipart/mixed; boundary="${boundary}"\n\n`;
|
const chunks = [];
|
||||||
rawEmail += `--${boundary}\n`;
|
for await (const chunk of messageStream) {
|
||||||
rawEmail += `Content-Type: text/html; charset="UTF-8"\n\n`;
|
chunks.push(chunk);
|
||||||
rawEmail += `${html}\n\n`;
|
|
||||||
|
|
||||||
if (attachments && attachments.length > 0) {
|
|
||||||
for (const attachment of attachments) {
|
|
||||||
const content = attachment.content; // Assumes content is base64
|
|
||||||
const mimeType =
|
|
||||||
mime.lookup(attachment.filename) || "application/octet-stream";
|
|
||||||
rawEmail += `--${boundary}\n`;
|
|
||||||
rawEmail += `Content-Type: ${mimeType}; name="${attachment.filename}"\n`;
|
|
||||||
rawEmail += `Content-Disposition: attachment; filename="${attachment.filename}"\n`;
|
|
||||||
rawEmail += `Content-Transfer-Encoding: base64\n\n`;
|
|
||||||
rawEmail += `${content}\n\n`;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
const finalMessageData = Buffer.concat(chunks);
|
||||||
rawEmail += `--${boundary}--`;
|
|
||||||
|
|
||||||
const command = new SendEmailCommand({
|
const command = new SendEmailCommand({
|
||||||
Content: {
|
Content: {
|
||||||
Raw: {
|
Raw: {
|
||||||
Data: Buffer.from(rawEmail),
|
Data: finalMessageData,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
ConfigurationSetName: configurationSetName,
|
ConfigurationSetName: configurationSetName,
|
||||||
|
25
pnpm-lock.yaml
generated
25
pnpm-lock.yaml
generated
@@ -130,7 +130,7 @@ importers:
|
|||||||
dependencies:
|
dependencies:
|
||||||
'@auth/prisma-adapter':
|
'@auth/prisma-adapter':
|
||||||
specifier: ^2.9.0
|
specifier: ^2.9.0
|
||||||
version: 2.9.0(@prisma/client@6.6.0)
|
version: 2.9.0(@prisma/client@6.6.0)(nodemailer@7.0.3)
|
||||||
'@aws-sdk/client-s3':
|
'@aws-sdk/client-s3':
|
||||||
specifier: ^3.797.0
|
specifier: ^3.797.0
|
||||||
version: 3.797.0
|
version: 3.797.0
|
||||||
@@ -220,7 +220,10 @@ importers:
|
|||||||
version: 15.3.1(react-dom@19.1.0)(react@19.1.0)
|
version: 15.3.1(react-dom@19.1.0)(react@19.1.0)
|
||||||
next-auth:
|
next-auth:
|
||||||
specifier: ^4.24.11
|
specifier: ^4.24.11
|
||||||
version: 4.24.11(next@15.3.1)(react-dom@19.1.0)(react@19.1.0)
|
version: 4.24.11(next@15.3.1)(nodemailer@7.0.3)(react-dom@19.1.0)(react@19.1.0)
|
||||||
|
nodemailer:
|
||||||
|
specifier: ^7.0.3
|
||||||
|
version: 7.0.3
|
||||||
pnpm:
|
pnpm:
|
||||||
specifier: ^10.9.0
|
specifier: ^10.9.0
|
||||||
version: 10.9.0
|
version: 10.9.0
|
||||||
@@ -282,6 +285,9 @@ importers:
|
|||||||
'@types/node':
|
'@types/node':
|
||||||
specifier: ^22.15.2
|
specifier: ^22.15.2
|
||||||
version: 22.15.2
|
version: 22.15.2
|
||||||
|
'@types/nodemailer':
|
||||||
|
specifier: ^6.4.17
|
||||||
|
version: 6.4.17
|
||||||
'@types/react':
|
'@types/react':
|
||||||
specifier: ^19.1.2
|
specifier: ^19.1.2
|
||||||
version: 19.1.2
|
version: 19.1.2
|
||||||
@@ -729,7 +735,7 @@ packages:
|
|||||||
'@types/json-schema': 7.0.15
|
'@types/json-schema': 7.0.15
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
/@auth/core@0.39.0:
|
/@auth/core@0.39.0(nodemailer@7.0.3):
|
||||||
resolution: {integrity: sha512-jusviw/sUSfAh6S/wjY5tRmJOq0Itd3ImF+c/b4HB9DfmfChtcfVJTNJeqCeExeCG8oh4PBKRsMQJsn2W6NhFQ==}
|
resolution: {integrity: sha512-jusviw/sUSfAh6S/wjY5tRmJOq0Itd3ImF+c/b4HB9DfmfChtcfVJTNJeqCeExeCG8oh4PBKRsMQJsn2W6NhFQ==}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
'@simplewebauthn/browser': ^9.0.1
|
'@simplewebauthn/browser': ^9.0.1
|
||||||
@@ -745,17 +751,18 @@ packages:
|
|||||||
dependencies:
|
dependencies:
|
||||||
'@panva/hkdf': 1.2.1
|
'@panva/hkdf': 1.2.1
|
||||||
jose: 6.0.10
|
jose: 6.0.10
|
||||||
|
nodemailer: 7.0.3
|
||||||
oauth4webapi: 3.5.0
|
oauth4webapi: 3.5.0
|
||||||
preact: 10.24.3
|
preact: 10.24.3
|
||||||
preact-render-to-string: 6.5.11(preact@10.24.3)
|
preact-render-to-string: 6.5.11(preact@10.24.3)
|
||||||
dev: false
|
dev: false
|
||||||
|
|
||||||
/@auth/prisma-adapter@2.9.0(@prisma/client@6.6.0):
|
/@auth/prisma-adapter@2.9.0(@prisma/client@6.6.0)(nodemailer@7.0.3):
|
||||||
resolution: {integrity: sha512-Vo/o3YJOa6x2gklQ+XE/JfUf8+CVSDci/nvlOc6psiIiUGigqnQpap2DAxD8brNNYGYKOJ4OAiSt6kQyk3WP3g==}
|
resolution: {integrity: sha512-Vo/o3YJOa6x2gklQ+XE/JfUf8+CVSDci/nvlOc6psiIiUGigqnQpap2DAxD8brNNYGYKOJ4OAiSt6kQyk3WP3g==}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
'@prisma/client': '>=2.26.0 || >=3 || >=4 || >=5 || >=6'
|
'@prisma/client': '>=2.26.0 || >=3 || >=4 || >=5 || >=6'
|
||||||
dependencies:
|
dependencies:
|
||||||
'@auth/core': 0.39.0
|
'@auth/core': 0.39.0(nodemailer@7.0.3)
|
||||||
'@prisma/client': 6.6.0(prisma@6.6.0)(typescript@5.8.3)
|
'@prisma/client': 6.6.0(prisma@6.6.0)(typescript@5.8.3)
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- '@simplewebauthn/browser'
|
- '@simplewebauthn/browser'
|
||||||
@@ -13215,7 +13222,7 @@ packages:
|
|||||||
engines: {node: '>= 0.4.0'}
|
engines: {node: '>= 0.4.0'}
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
/next-auth@4.24.11(next@15.3.1)(react-dom@19.1.0)(react@19.1.0):
|
/next-auth@4.24.11(next@15.3.1)(nodemailer@7.0.3)(react-dom@19.1.0)(react@19.1.0):
|
||||||
resolution: {integrity: sha512-pCFXzIDQX7xmHFs4KVH4luCjaCbuPRtZ9oBUjUhOk84mZ9WVPf94n87TxYI4rSRf9HmfHEF8Yep3JrYDVOo3Cw==}
|
resolution: {integrity: sha512-pCFXzIDQX7xmHFs4KVH4luCjaCbuPRtZ9oBUjUhOk84mZ9WVPf94n87TxYI4rSRf9HmfHEF8Yep3JrYDVOo3Cw==}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
'@auth/core': 0.34.2
|
'@auth/core': 0.34.2
|
||||||
@@ -13234,6 +13241,7 @@ packages:
|
|||||||
cookie: 0.7.2
|
cookie: 0.7.2
|
||||||
jose: 4.15.9
|
jose: 4.15.9
|
||||||
next: 15.3.1(react-dom@19.1.0)(react@19.1.0)
|
next: 15.3.1(react-dom@19.1.0)(react@19.1.0)
|
||||||
|
nodemailer: 7.0.3
|
||||||
oauth: 0.9.15
|
oauth: 0.9.15
|
||||||
openid-client: 5.7.1
|
openid-client: 5.7.1
|
||||||
preact: 10.26.5
|
preact: 10.26.5
|
||||||
@@ -13397,6 +13405,11 @@ packages:
|
|||||||
engines: {node: '>=6.0.0'}
|
engines: {node: '>=6.0.0'}
|
||||||
dev: false
|
dev: false
|
||||||
|
|
||||||
|
/nodemailer@7.0.3:
|
||||||
|
resolution: {integrity: sha512-Ajq6Sz1x7cIK3pN6KesGTah+1gnwMnx5gKl3piQlQQE/PwyJ4Mbc8is2psWYxK3RJTVeqsDaCv8ZzXLCDHMTZw==}
|
||||||
|
engines: {node: '>=6.0.0'}
|
||||||
|
dev: false
|
||||||
|
|
||||||
/nopt@7.2.1:
|
/nopt@7.2.1:
|
||||||
resolution: {integrity: sha512-taM24ViiimT/XntxbPyJQzCG+p4EKOpgD3mxFwW38mGjVUrfERQOeY4EDHjdnptttfHuHQXFx+lTP08Q+mLa/w==}
|
resolution: {integrity: sha512-taM24ViiimT/XntxbPyJQzCG+p4EKOpgD3mxFwW38mGjVUrfERQOeY4EDHjdnptttfHuHQXFx+lTP08Q+mLa/w==}
|
||||||
engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0}
|
engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0}
|
||||||
|
Reference in New Issue
Block a user