Skip to content
Open
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
2 changes: 1 addition & 1 deletion packages/database/auth/auth-options.ts
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@ export const authOptions = (): NextAuthOptions => {
async sendVerificationRequest({ identifier, token }) {
console.log("sendVerificationRequest");

if (!serverEnv().RESEND_API_KEY) {
if (!serverEnv().RESEND_API_KEY && !serverEnv().SMTP_HOST) {
console.log("\n");
console.log(
"━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━",
Expand Down
67 changes: 59 additions & 8 deletions packages/database/emails/config.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,33 @@
import { buildEnv, serverEnv } from "@cap/env";
import { render } from "@react-email/render";
import nodemailer from "nodemailer";
import type { JSXElementConstructor, ReactElement } from "react";
import { Resend } from "resend";

export const resend = () =>
serverEnv().RESEND_API_KEY ? new Resend(serverEnv().RESEND_API_KEY) : null;

let _smtpTransport: ReturnType<typeof nodemailer.createTransport> | null | undefined;

export const smtp = () => {
if (_smtpTransport !== undefined) return _smtpTransport;
const env = serverEnv();
if (!env.SMTP_HOST) {
_smtpTransport = null;
return null;
}
_smtpTransport = nodemailer.createTransport({
host: env.SMTP_HOST,
port: env.SMTP_PORT,
secure: env.SMTP_SECURE,
auth:
env.SMTP_USER && env.SMTP_PASS
? { user: env.SMTP_USER, pass: env.SMTP_PASS }
: undefined,
});
return _smtpTransport;
};
Comment on lines +12 to +29
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 New transporter created on every sendEmail() call

smtp() calls nodemailer.createTransport() afresh for every email, discarding the internal connection pool each time. Following the memoisation pattern already used for serverEnv() would keep a single pooled transporter alive for the process lifetime and avoid repeated setup overhead.

Prompt To Fix With AI
This is a comment left during a code review.
Path: packages/database/emails/config.ts
Line: 10-22

Comment:
**New transporter created on every `sendEmail()` call**

`smtp()` calls `nodemailer.createTransport()` afresh for every email, discarding the internal connection pool each time. Following the memoisation pattern already used for `serverEnv()` would keep a single pooled transporter alive for the process lifetime and avoid repeated setup overhead.

How can I resolve this? If you propose a fix, please make it concise.


export const sendEmail = async ({
email,
subject,
Expand All @@ -26,23 +49,51 @@ export const sendEmail = async ({
replyTo?: string;
fromOverride?: string;
}) => {
const r = resend();
if (!r) {
return Promise.resolve();
}

if (marketing && !buildEnv.NEXT_PUBLIC_IS_CAP) return;
let from;

let from: string;
if (fromOverride) from = fromOverride;
else if (marketing) from = "Richie from Cap <richie@send.cap.so>";
else if (buildEnv.NEXT_PUBLIC_IS_CAP)
from = "Cap Auth <no-reply@auth.cap.so>";
else from = `auth@${serverEnv().RESEND_FROM_DOMAIN}`;
else if (serverEnv().SMTP_FROM) from = serverEnv().SMTP_FROM!;
else if (serverEnv().RESEND_FROM_DOMAIN)
from = `auth@${serverEnv().RESEND_FROM_DOMAIN}`;
else {
// No from-address configured. Most SMTP servers reject "noreply@localhost"
// as invalid sender, which would silently drop the email. Fail loudly instead.
throw new Error(
"No email from-address configured. Set SMTP_FROM (when using SMTP) or RESEND_FROM_DOMAIN (when using Resend).",
);
}

// Resend has a sandbox sink at delivered@resend.dev for test mode;
// over SMTP we just use the real recipient
const to = test && !smtp() ? "delivered@resend.dev" : email;

// Try SMTP first if configured (preferred for self-hosted)
const transport = smtp();
if (transport) {
const html = await render(react);
return transport.sendMail({
from,
to,
subject,
html,
cc: test ? undefined : cc,
replyTo,
}) as any;
}

// Fall back to Resend if configured
const r = resend();
if (!r) {
return Promise.resolve();
}

return r.emails.send({
from,
to: test ? "delivered@resend.dev" : email,
to,
subject,
react,
scheduledAt,
Expand Down
9 changes: 9 additions & 0 deletions packages/env/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,15 @@ function createServerEnv() {
RESEND_API_KEY: z.string().optional(),
RESEND_FROM_DOMAIN: z.string().optional(),

// SMTP configuration (alternative to Resend)
// If SMTP_HOST is set, emails will be sent via SMTP instead of Resend
SMTP_HOST: z.string().optional().describe("SMTP server hostname"),
SMTP_PORT: z.coerce.number().optional().default(587).describe("SMTP server port"),
SMTP_USER: z.string().optional().describe("SMTP username"),
SMTP_PASS: z.string().optional().describe("SMTP password"),
SMTP_FROM: z.string().optional().describe("Default From address (e.g. cap@mail.example.com)"),
SMTP_SECURE: boolString(false).describe("Use TLS/SSL (true for port 465, false for 587/STARTTLS)"),

/// S3 configuration
// Though they are prefixed with `CAP_AWS`, these don't have to be
// for AWS, and can instead be for any S3-compatible service
Expand Down