diff --git a/package-lock.json b/package-lock.json index ac1c7d2f1..4f4159749 100644 --- a/package-lock.json +++ b/package-lock.json @@ -49,7 +49,7 @@ "inngest-cli": "^1.17.9", "lint-staged": "^16.2.7", "nanoid": "^5.1.6", - "nodemailer": "^8.0.5", + "nodemailer": "^9.0.0", "pdfjs-dist": "5.4.296", "pino": "^9.14.0", "pino-pretty": "^13.1.2", @@ -3048,15 +3048,15 @@ "link": true }, "node_modules/@documenso/nodemailer-resend": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/@documenso/nodemailer-resend/-/nodemailer-resend-4.0.0.tgz", - "integrity": "sha512-o2Wpz8jJXOov+CbEzWUhqXlBsdx/l/W0au054j5HUExaLpS+sCoJfdOM0A5uHGTInWOxqbNB0WSvxf9dWAzVSg==", + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/@documenso/nodemailer-resend/-/nodemailer-resend-5.0.0.tgz", + "integrity": "sha512-PDC7Yjzg35cS5i2TSJEZSP9j86BsYv4pEZOODurkK8+KFd3ymtWUqJrM6PrSNQ4MQpFp7chHlbBFP0ALYl8n0A==", "license": "MIT", "dependencies": { - "resend": "^4.0.0" + "resend": "^4.8.0" }, "peerDependencies": { - "nodemailer": "^6.9.3" + "nodemailer": "^9.0.0" } }, "node_modules/@documenso/nodemailer-resend/node_modules/@react-email/render": { @@ -24494,9 +24494,9 @@ "license": "MIT" }, "node_modules/nodemailer": { - "version": "8.0.11", - "resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-8.0.11.tgz", - "integrity": "sha512-nrO/pDAUKl+wXX+lx16tDLbnm0fW6sK/x8mgohaCpg+CdCEl482bD4tCuAZk2DyliruiNTIZxRCoWkDqJEnAiA==", + "version": "9.0.3", + "resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-9.0.3.tgz", + "integrity": "sha512-n+YP+NKwR5zRWa60k3GiQ6Q3B4KXCoAw40dAKeCtYn020iNN74aWK2liXIC3ZEATeGql7we3tE3t8QwhY0eskw==", "license": "MIT-0", "engines": { "node": ">=6.0.0" @@ -31270,7 +31270,7 @@ "version": "0.0.0", "license": "MIT", "dependencies": { - "@documenso/nodemailer-resend": "4.0.0", + "@documenso/nodemailer-resend": "5.0.0", "@documenso/tailwind-config": "*", "@react-email/body": "0.2.0", "@react-email/button": "0.2.0", @@ -31291,7 +31291,7 @@ "@react-email/section": "0.0.16", "@react-email/tailwind": "^2.0.1", "@react-email/text": "0.1.5", - "nodemailer": "^8.0.5", + "nodemailer": "^9.0.0", "react-email": "^5.0.6", "resend": "^6.5.2" }, diff --git a/package.json b/package.json index 808577cbc..2800c7f84 100644 --- a/package.json +++ b/package.json @@ -66,7 +66,7 @@ "inngest-cli": "^1.17.9", "lint-staged": "^16.2.7", "nanoid": "^5.1.6", - "nodemailer": "^8.0.5", + "nodemailer": "^9.0.0", "pdfjs-dist": "5.4.296", "pino": "^9.14.0", "pino-pretty": "^13.1.2", diff --git a/packages/email/package.json b/packages/email/package.json index bb549f70b..5fbc4a88c 100644 --- a/packages/email/package.json +++ b/packages/email/package.json @@ -17,7 +17,7 @@ "clean": "rimraf node_modules" }, "dependencies": { - "@documenso/nodemailer-resend": "4.0.0", + "@documenso/nodemailer-resend": "5.0.0", "@documenso/tailwind-config": "*", "@react-email/body": "0.2.0", "@react-email/button": "0.2.0", @@ -38,7 +38,7 @@ "@react-email/section": "0.0.16", "@react-email/tailwind": "^2.0.1", "@react-email/text": "0.1.5", - "nodemailer": "^8.0.5", + "nodemailer": "^9.0.0", "react-email": "^5.0.6", "resend": "^6.5.2" }, diff --git a/packages/email/transports/mailchannels.ts b/packages/email/transports/mailchannels.ts index 93e82804d..923346f79 100644 --- a/packages/email/transports/mailchannels.ts +++ b/packages/email/transports/mailchannels.ts @@ -3,6 +3,8 @@ import type { SentMessageInfo, Transport } from 'nodemailer'; import type { Address } from 'nodemailer/lib/mailer'; import type MailMessage from 'nodemailer/lib/mailer/mail-message'; +import { normalizeMailHeaders } from './normalize-headers'; + const VERSION = '1.0.0'; type NodeMailerAddress = string | Address | Array | undefined; @@ -54,6 +56,7 @@ export class MailChannelsTransport implements Transport { const mailBcc = this.toMailChannelsAddresses(mail.data.bcc); const [from] = this.toMailChannelsAddresses(mail.data.from); + const [replyTo] = this.toMailChannelsAddresses(mail.data.replyTo); if (!from) { return callback(new Error('Missing required field "from"'), null); @@ -72,6 +75,8 @@ export class MailChannelsTransport implements Transport { headers: requestHeaders, body: JSON.stringify({ from: from, + reply_to: replyTo, + headers: normalizeMailHeaders(mail.data.headers), subject: mail.data.subject, personalizations: [ { diff --git a/packages/email/transports/normalize-headers.ts b/packages/email/transports/normalize-headers.ts new file mode 100644 index 000000000..4dca6c8f1 --- /dev/null +++ b/packages/email/transports/normalize-headers.ts @@ -0,0 +1,55 @@ +import type Mail from 'nodemailer/lib/mailer'; + +/** + * Normalizes nodemailer mail headers into the flat `Record` + * shape accepted by HTTP email APIs such as Resend and MailChannels. + * + * Kept in sync with `toResendHeaders` in the `@documenso/nodemailer-resend` + * package, which applies the same normalization for the Resend transport. + */ +export const normalizeMailHeaders = (headers: Mail.Options['headers']): Record | undefined => { + if (!headers) { + return undefined; + } + + const normalized: Record = {}; + + const appendHeader = (key: string, value: unknown) => { + if (value === null || value === undefined) { + return; + } + + const stringValue = String(value); + + normalized[key] = normalized[key] ? `${normalized[key]}, ${stringValue}` : stringValue; + }; + + if (Array.isArray(headers)) { + for (const { key, value } of headers) { + appendHeader(key, value); + } + } else { + for (const [key, value] of Object.entries(headers)) { + if (Array.isArray(value)) { + for (const item of value) { + appendHeader(key, item); + } + + continue; + } + + if (typeof value === 'object' && value !== null) { + appendHeader(key, value.value); + continue; + } + + appendHeader(key, value); + } + } + + if (Object.keys(normalized).length === 0) { + return undefined; + } + + return normalized; +};