Skip to content
Open
Changes from 1 commit
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
83 changes: 50 additions & 33 deletions packages/server/src/lib/email.ts
Original file line number Diff line number Diff line change
@@ -1,38 +1,43 @@
import nodemailer from 'nodemailer';
import type { Transporter } from 'nodemailer';
import prisma from './db.js';
import nodemailer from "nodemailer";
import type { Transporter } from "nodemailer";
import prisma from "./db.js";

let cachedTransporter: Transporter | null = null;
let cachedFrom: string = '';
let cachedFrom: string = "";
let cacheExpiry = 0;
const CACHE_TTL = 5 * 60 * 1000; // 5 minutes

async function getMailConfig(): Promise<{ transporter: Transporter; from: string }> {
async function getMailConfig(): Promise<{
transporter: Transporter;
from: string;
}> {
const now = Date.now();
if (cachedTransporter && now < cacheExpiry) {
return { transporter: cachedTransporter, from: cachedFrom };
}

let host = process.env.SMTP_HOST || 'localhost';
let port = parseInt(process.env.SMTP_PORT || '1025');
let host = process.env.SMTP_HOST || "localhost";
let port = parseInt(process.env.SMTP_PORT || "1025");
let secure = false;
let user = process.env.SMTP_USER;
let pass = process.env.SMTP_PASS;
let senderName = 'KitchenAsty';
let senderEmail = 'noreply@kitchenasty.com';
let senderName = "KitchenAsty";
let senderEmail = "noreply@kitchenasty.com";
let requireTLS = false;

try {
const settings = await prisma.siteSettings.findUnique({ where: { id: 'default' } });
const settings = await prisma.siteSettings.findUnique({
where: { id: "default" },
});
const mail = (settings?.mailSettings as Record<string, any>) || {};
if (mail.smtpHost) host = mail.smtpHost;
if (mail.smtpPort) port = mail.smtpPort;
if (mail.smtpUser) user = mail.smtpUser;
if (mail.smtpPass) pass = mail.smtpPass;
if (mail.senderName) senderName = mail.senderName;
if (mail.senderEmail) senderEmail = mail.senderEmail;
if (mail.encryption === 'ssl') secure = true;
if (mail.encryption === 'tls') requireTLS = true;
if (mail.encryption === "ssl") secure = true;
if (mail.encryption === "tls") requireTLS = true;
} catch {
// DB unavailable — fall back to env vars
}
Expand Down Expand Up @@ -67,7 +72,7 @@ interface EmailOptions {
}

export async function sendEmail(options: EmailOptions): Promise<void> {
if (process.env.NODE_ENV === 'test') return;
if (process.env.NODE_ENV === "test") return;

try {
const { transporter, from } = await getMailConfig();
Expand All @@ -78,10 +83,19 @@ export async function sendEmail(options: EmailOptions): Promise<void> {
html: options.html,
});
} catch (err) {
console.error('Failed to send email:', err);
console.error("Failed to send email:", err);
}
}

async function getSiteName(name: String | undefined) {
const siteName = await prisma.siteSettings.findFirst({
where: { siteName: `${name}` },
Comment thread
aboodJS marked this conversation as resolved.
Outdated
});
console.log(siteName);

return siteName || "KitchenAsty";
}

// Email Templates

export function orderConfirmationEmail(order: {
Expand All @@ -90,16 +104,19 @@ export function orderConfirmationEmail(order: {
total: number;
items: { name: string; quantity: number; subtotal: number }[];
}): { subject: string; html: string } {
const itemRows = order.items.map((i) =>
`<tr><td style="padding:8px;border-bottom:1px solid #eee">${i.quantity}x ${i.name}</td><td style="padding:8px;border-bottom:1px solid #eee;text-align:right">$${i.subtotal.toFixed(2)}</td></tr>`
).join('');
const itemRows = order.items
.map(
(i) =>
`<tr><td style="padding:8px;border-bottom:1px solid #eee">${i.quantity}x ${i.name}</td><td style="padding:8px;border-bottom:1px solid #eee;text-align:right">$${i.subtotal.toFixed(2)}</td></tr>`,
)
.join("");

return {
subject: `Order Confirmed - #${order.orderNumber}`,
html: `
<div style="max-width:600px;margin:0 auto;font-family:sans-serif">
<div style="background:#f97316;color:white;padding:20px;text-align:center;border-radius:8px 8px 0 0">
<h1 style="margin:0;font-size:24px">KitchenAsty</h1>
<h1 style="margin:0;font-size:24px">${getSiteName(process.env.EMAIL_FROM)}</h1>
</div>
<div style="padding:24px;background:white;border:1px solid #e5e7eb;border-top:none;border-radius:0 0 8px 8px">
<h2 style="margin:0 0 8px">Order Confirmed!</h2>
Expand All @@ -122,28 +139,28 @@ export function orderStatusEmail(order: {
status: string;
}): { subject: string; html: string } {
const statusMessages: Record<string, string> = {
CONFIRMED: 'Your order has been confirmed and will be prepared soon.',
PREPARING: 'Your order is now being prepared!',
READY: 'Your order is ready!',
OUT_FOR_DELIVERY: 'Your order is on its way!',
DELIVERED: 'Your order has been delivered. Enjoy!',
PICKED_UP: 'Your order has been picked up. Enjoy!',
CANCELLED: 'Your order has been cancelled.',
CONFIRMED: "Your order has been confirmed and will be prepared soon.",
PREPARING: "Your order is now being prepared!",
READY: "Your order is ready!",
OUT_FOR_DELIVERY: "Your order is on its way!",
DELIVERED: "Your order has been delivered. Enjoy!",
PICKED_UP: "Your order has been picked up. Enjoy!",
CANCELLED: "Your order has been cancelled.",
};

return {
subject: `Order #${order.orderNumber} - ${order.status.replace(/_/g, ' ')}`,
subject: `Order #${order.orderNumber} - ${order.status.replace(/_/g, " ")}`,
html: `
<div style="max-width:600px;margin:0 auto;font-family:sans-serif">
<div style="background:#f97316;color:white;padding:20px;text-align:center;border-radius:8px 8px 0 0">
<h1 style="margin:0;font-size:24px">KitchenAsty</h1>
<h1 style="margin:0;font-size:24px">${getSiteName(process.env.EMAIL_FROM)}</h1>
</div>
<div style="padding:24px;background:white;border:1px solid #e5e7eb;border-top:none;border-radius:0 0 8px 8px">
<h2 style="margin:0 0 8px">Order Update</h2>
<p style="color:#6b7280;margin:0 0 16px">Order <strong>#${order.orderNumber}</strong></p>
<div style="background:#f3f4f6;padding:16px;border-radius:8px;margin-bottom:16px">
<p style="margin:0;font-size:18px;font-weight:bold">${order.status.replace(/_/g, ' ')}</p>
<p style="margin:8px 0 0;color:#6b7280">${statusMessages[order.status] || 'Your order status has been updated.'}</p>
<p style="margin:0;font-size:18px;font-weight:bold">${order.status.replace(/_/g, " ")}</p>
<p style="margin:8px 0 0;color:#6b7280">${statusMessages[order.status] || "Your order status has been updated."}</p>
</div>
</div>
</div>
Expand All @@ -157,15 +174,15 @@ export function staffInvitationEmail(invite: {
inviteLink: string;
}): { subject: string; html: string } {
return {
subject: 'You\'re Invited to Join KitchenAsty',
subject: "You're Invited to Join KitchenAsty",
html: `
<div style="max-width:600px;margin:0 auto;font-family:sans-serif">
<div style="background:#f97316;color:white;padding:20px;text-align:center;border-radius:8px 8px 0 0">
<h1 style="margin:0;font-size:24px">KitchenAsty</h1>
<h1 style="margin:0;font-size:24px">${getSiteName(process.env.EMAIL_FROM)}</h1>
</div>
<div style="padding:24px;background:white;border:1px solid #e5e7eb;border-top:none;border-radius:0 0 8px 8px">
<h2 style="margin:0 0 8px">You're Invited!</h2>
<p style="color:#6b7280;margin:0 0 16px">You've been invited to join the KitchenAsty team as <strong>${invite.role.replace(/_/g, ' ')}</strong>.</p>
<p style="color:#6b7280;margin:0 0 16px">You've been invited to join the KitchenAsty team as <strong>${invite.role.replace(/_/g, " ")}</strong>.</p>
<div style="text-align:center;margin:24px 0">
<a href="${invite.inviteLink}" style="display:inline-block;background:#f97316;color:white;padding:12px 32px;border-radius:8px;text-decoration:none;font-weight:bold;font-size:16px">Accept Invitation</a>
</div>
Expand All @@ -187,7 +204,7 @@ export function reservationConfirmationEmail(reservation: {
html: `
<div style="max-width:600px;margin:0 auto;font-family:sans-serif">
<div style="background:#f97316;color:white;padding:20px;text-align:center;border-radius:8px 8px 0 0">
<h1 style="margin:0;font-size:24px">KitchenAsty</h1>
<h1 style="margin:0;font-size:24px">${getSiteName(process.env.EMAIL_FROM)}</h1>
</div>
<div style="padding:24px;background:white;border:1px solid #e5e7eb;border-top:none;border-radius:0 0 8px 8px">
<h2 style="margin:0 0 16px">Reservation Confirmed!</h2>
Expand Down