Nodemailer Setup
beginnerFree email sending with Nodemailer. Supports SMTP, Gmail, and other providers.
emailnodemailersmtptransactional
Tested on⬢20▲16⚛19TS5.9
$ bunx sinew add email/nodemailerInteractive demo coming soon
1The Problem
Email services charge per email and add vendor lock-in:
- Resend, SendGrid charge based on volume
- API changes require code updates
- No control over email infrastructure
2The Solution
Use Nodemailer with any SMTP provider for full control and zero per-email costs.
3Files
lib/email.ts
lib/email.tsTypeScript
import nodemailer from "nodemailer";
const transporter = nodemailer.createTransport({
host: process.env.SMTP_HOST,
port: parseInt(process.env.SMTP_PORT || "587"),
secure: process.env.SMTP_SECURE === "true",
auth: {
user: process.env.SMTP_USER,
pass: process.env.SMTP_PASSWORD,
},
});
interface SendEmailOptions {
to: string | string[];
subject: string;
html?: string;
text?: string;
from?: string;
replyTo?: string;
attachments?: Array<{
filename: string;
content: Buffer | string;
contentType?: string;
}>;
}
const DEFAULT_FROM = process.env.EMAIL_FROM || "noreply@example.com";
export async function sendEmail({
to,
subject,
html,
text,
from = DEFAULT_FROM,
replyTo,
attachments,
}: SendEmailOptions) {
const info = await transporter.sendMail({
from,
to: Array.isArray(to) ? to.join(", ") : to,
subject,
html,
text,
replyTo,
attachments,
});
console.info(`Email sent: ${info.messageId}`);
return info;
}
// Verify connection on startup
export async function verifyEmailConnection() {
try {
await transporter.verify();
console.info("SMTP connection verified");
return true;
} catch (error) {
console.error("SMTP connection failed:", error);
return false;
}
}lib/email-templates.ts
lib/email-templates.tsTypeScript
interface WelcomeEmailProps {
name: string;
loginUrl: string;
}
export function welcomeEmailTemplate({ name, loginUrl }: WelcomeEmailProps) {
return {
subject: "Welcome to Our Platform!",
html: `
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
</head>
<body style="font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; background-color: #f6f9fc; padding: 40px 20px;">
<div style="max-width: 560px; margin: 0 auto; background-color: #ffffff; border-radius: 8px; padding: 40px;">
<h1 style="color: #1a1a1a; font-size: 24px; margin: 0 0 20px;">Welcome, ${name}!</h1>
<p style="color: #4a4a4a; font-size: 16px; line-height: 24px;">
We're excited to have you on board. Get started by exploring your dashboard.
</p>
<div style="text-align: center; margin: 30px 0;">
<a href="${loginUrl}" style="display: inline-block; background-color: #e85a2c; color: #ffffff; padding: 12px 24px; border-radius: 6px; text-decoration: none; font-weight: 600;">
Go to Dashboard
</a>
</div>
<p style="color: #8a8a8a; font-size: 14px;">
If you have any questions, reply to this email.
</p>
</div>
</body>
</html>
`,
text: `Welcome, ${name}!\n\nWe're excited to have you on board. Get started by exploring your dashboard: ${loginUrl}`,
};
}
interface PasswordResetProps {
resetUrl: string;
expiresIn: string;
}
export function passwordResetTemplate({ resetUrl, expiresIn }: PasswordResetProps) {
return {
subject: "Reset Your Password",
html: `
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
</head>
<body style="font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; background-color: #f6f9fc; padding: 40px 20px;">
<div style="max-width: 560px; margin: 0 auto; background-color: #ffffff; border-radius: 8px; padding: 40px;">
<h1 style="color: #1a1a1a; font-size: 24px; margin: 0 0 20px;">Reset Your Password</h1>
<p style="color: #4a4a4a; font-size: 16px; line-height: 24px;">
We received a request to reset your password. Click the button below to create a new password.
</p>
<div style="text-align: center; margin: 30px 0;">
<a href="${resetUrl}" style="display: inline-block; background-color: #e85a2c; color: #ffffff; padding: 12px 24px; border-radius: 6px; text-decoration: none; font-weight: 600;">
Reset Password
</a>
</div>
<p style="color: #4a4a4a; font-size: 14px;">
This link will expire in ${expiresIn}. If you didn't request this, you can safely ignore this email.
</p>
</div>
</body>
</html>
`,
text: `Reset Your Password\n\nClick this link to reset your password: ${resetUrl}\n\nThis link expires in ${expiresIn}.`,
};
}app/api/auth/send-welcome/route.ts
app/api/auth/send-welcome/route.tsTypeScript
import { NextRequest, NextResponse } from "next/server";
import { sendEmail } from "@/lib/email";
import { welcomeEmailTemplate } from "@/lib/email-templates";
export async function POST(req: NextRequest) {
const { email, name } = await req.json();
try {
const template = welcomeEmailTemplate({
name,
loginUrl: `${process.env.NEXTAUTH_URL}/login`,
});
await sendEmail({
to: email,
...template,
});
return NextResponse.json({ success: true });
} catch (error) {
console.error("Failed to send welcome email:", error);
return NextResponse.json({ error: "Failed to send email" }, { status: 500 });
}
}.env.example
.env.exampleBash
# SMTP Configuration
SMTP_HOST="smtp.gmail.com"
SMTP_PORT="587"
SMTP_SECURE="false"
SMTP_USER="your-email@gmail.com"
SMTP_PASSWORD="your-app-password"
EMAIL_FROM="Your App <your-email@gmail.com>"4Dependencies
$ bun add nodemailer$ bun add -D @types/nodemailer5Configuration
Gmail Setup
- Enable 2-Factor Authentication on your Google account
- Generate an App Password at https://myaccount.google.com/apppasswords
- Use the app password as
SMTP_PASSWORD
Other SMTP Providers
Amazon SES:
SMTP_HOST="email-smtp.us-east-1.amazonaws.com"
SMTP_PORT="587"Bash
Mailgun:
SMTP_HOST="smtp.mailgun.org"
SMTP_PORT="587"Bash
6Usage
Sending a Simple Email
import { sendEmail } from "@/lib/email";
await sendEmail({
to: "user@example.com",
subject: "Hello!",
html: "<p>This is a test email</p>",
text: "This is a test email",
});TypeScript
Sending with Attachments
await sendEmail({
to: "user@example.com",
subject: "Your Invoice",
html: "<p>Please find your invoice attached.</p>",
attachments: [
{
filename: "invoice.pdf",
content: pdfBuffer,
contentType: "application/pdf",
},
],
});TypeScript
7Troubleshooting
Gmail blocking sign-ins
- Use App Passwords instead of your account password
- Enable "Less secure app access" is no longer supported
Emails going to spam
- Set up SPF, DKIM, and DMARC records for your domain
- Use a consistent "from" address
- Avoid spam trigger words in subject lines