Files
property-management-network/lib/email/send.ts
T

264 lines
8.3 KiB
TypeScript
Raw Normal View History

import { getTransporter, emailConfigured, FROM_EMAIL, APP_NAME } from "./client"
import {
BRAND,
emailShell,
detailTable,
statusBadge,
paragraph,
escapeHtml,
htmlToText,
} from "./layout"
// Re-exported so existing importers (`@/lib/email/send`) keep working.
export { escapeHtml }
interface SendEmailOptions {
to: string
subject: string
html: string
/** Optional plain-text part. Auto-derived from `html` when omitted. */
text?: string
from?: string
}
export async function sendEmail({ to, subject, html, text, from }: SendEmailOptions) {
if (!emailConfigured()) {
console.warn(`[email] SMTP not configured — skipping "${subject}" to ${to}`)
return { success: false, error: "email not configured" }
}
try {
const info = await getTransporter().sendMail({
from: from ?? `${APP_NAME} <${FROM_EMAIL}>`,
to,
subject,
html,
// A plain-text alternative improves inbox placement and gives clients
// that can't render HTML something clean to show.
text: text ?? htmlToText(html),
})
return { success: true, id: info.messageId }
} catch (error) {
console.error("Email send failed:", error)
return { success: false, error }
}
}
// ── Email templates ──────────────────────────────────────────────
export function rentDueReminderHtml({
tenantName,
propertyName,
unitNumber,
amount,
dueDate,
paymentLink,
}: {
tenantName: string
propertyName: string
unitNumber: string
amount: string
dueDate: string
paymentLink?: string
}) {
return emailShell({
preheader: `Your rent of ${amount} is due on ${dueDate}.`,
eyebrow: "Rent reminder",
accent: BRAND.indigo,
title: "Your rent is due soon",
intro: `Hi ${escapeHtml(tenantName)}, this is a friendly reminder about your upcoming rent payment.`,
body: detailTable([
{ label: "Property", value: `${propertyName} — Unit ${unitNumber}` },
{ label: "Amount due", value: amount, accent: true },
{ label: "Due date", value: dueDate },
]),
button: paymentLink ? { href: paymentLink, label: "Pay rent now" } : undefined,
footerNote: paymentLink
? "If the button doesn't work, contact your landlord for alternative payment options."
: "Please arrange payment before the due date. Reach out to your landlord with any questions.",
})
}
export function rentOverdueHtml({
tenantName,
propertyName,
unitNumber,
amount,
dueDate,
}: {
tenantName: string
propertyName: string
unitNumber: string
amount: string
dueDate: string
}) {
return emailShell({
preheader: `Your rent payment of ${amount} is now overdue.`,
eyebrow: "Action needed",
accent: BRAND.red,
title: "Your rent is overdue",
intro: `Hi ${escapeHtml(tenantName)}, our records show the payment below hasn't been received yet.`,
body:
detailTable(
[
{ label: "Property", value: `${propertyName} — Unit ${unitNumber}` },
{ label: "Amount due", value: amount, accent: true },
{ label: "Was due", value: dueDate, accent: true },
],
BRAND.red
) +
paragraph(
"Please make payment as soon as possible to avoid any late fees. If you've already paid, you can disregard this notice."
),
})
}
export function leaseExpiryHtml({
tenantName,
propertyName,
unitNumber,
leaseEnd,
daysLeft,
}: {
tenantName: string
propertyName: string
unitNumber: string
leaseEnd: string
daysLeft: number
}) {
return emailShell({
preheader: `Your lease ends on ${leaseEnd} (${daysLeft} days away).`,
eyebrow: "Lease update",
accent: BRAND.amber,
title: "Your lease is expiring soon",
intro: `Hi ${escapeHtml(tenantName)}, your current lease is coming to an end.`,
body:
detailTable(
[
{ label: "Property", value: `${propertyName} — Unit ${unitNumber}` },
{ label: "Lease ends", value: leaseEnd, accent: true },
{ label: "Time remaining", value: `${daysLeft} days` },
],
BRAND.amber
) + paragraph("Please contact your landlord to discuss renewal options or next steps."),
})
}
export function maintenanceUpdateHtml({
tenantName,
title,
status,
resolutionNotes,
}: {
tenantName: string
title: string
status: string
resolutionNotes?: string
}) {
const prettyStatus = status.replace(/_/g, " ")
const fontStack =
"-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,Helvetica,Arial,sans-serif"
return emailShell({
preheader: `Your maintenance request is now "${prettyStatus}".`,
eyebrow: "Maintenance",
accent: BRAND.indigo,
title: "Update on your maintenance request",
intro: `Hi ${escapeHtml(tenantName)}, there's an update on your request.`,
body:
`<table role="presentation" width="100%" cellspacing="0" cellpadding="0" border="0" style="margin:8px 0 20px;background:${BRAND.panel};border:1px solid ${BRAND.line};border-radius:12px;">
<tr><td style="padding:18px 22px;">
<p style="margin:0 0 12px;font-family:${fontStack};font-size:15px;font-weight:600;color:${BRAND.ink};">${escapeHtml(title)}</p>
${statusBadge(prettyStatus, BRAND.indigo)}
</td></tr>
</table>` +
(resolutionNotes ? paragraph(escapeHtml(resolutionNotes), { muted: true }) : ""),
})
}
export function resetPasswordHtml(url: string) {
return emailShell({
preheader: "Reset your Property Management Network password.",
eyebrow: "Security",
title: "Reset your password",
intro:
"We received a request to reset your password. Click the button below to choose a new one — this link will expire shortly for your security.",
button: { href: url, label: "Reset password" },
footerNote:
"If you didn't request a password reset, you can safely ignore this email — your password won't change.",
})
}
export function verifyEmailHtml(url: string) {
return emailShell({
preheader: "Confirm your email to finish setting up your account.",
eyebrow: "Welcome",
title: "Verify your email address",
intro:
"Thanks for signing up! Please confirm your email address to finish setting up your account.",
button: { href: url, label: "Verify email" },
footerNote: "If you didn't create an account, you can safely ignore this email.",
})
}
export function teamInviteHtml({
inviterName,
inviteUrl,
role,
}: {
inviterName: string
inviteUrl: string
role: "member" | "viewer"
}) {
const roleLabel = role === "viewer" ? "view" : "manage"
return emailShell({
preheader: `${inviterName} invited you to their property portfolio.`,
eyebrow: "Team invitation",
title: "You've been invited to a team",
intro: `<strong style="color:${BRAND.ink};">${escapeHtml(inviterName)}</strong> has invited you to ${escapeHtml(roleLabel)} their property portfolio on ${escapeHtml(APP_NAME)}.`,
button: { href: inviteUrl, label: "Accept invitation" },
footerNote:
"If you don't have an account yet, you'll be asked to sign in or sign up first.",
})
}
export function followUpHtml(message: string) {
return emailShell({
preheader: message.slice(0, 140),
accent: BRAND.indigo,
title: "A quick note from your landlord",
body: paragraph(escapeHtml(message).replace(/\n/g, "<br/>")),
footerNote: "Sent automatically by your landlord's follow-up system.",
})
}
export function paymentLinkHtml({
tenantName,
amount,
dueDate,
propertyLabel,
senderName,
paymentLink,
}: {
tenantName: string
amount: string
dueDate: string
propertyLabel: string
senderName: string
paymentLink?: string
}) {
return emailShell({
preheader: `Rent payment of ${amount} due ${dueDate}.`,
eyebrow: "Rent payment",
accent: BRAND.indigo,
title: "Your rent payment is due",
intro: `Hi ${escapeHtml(tenantName)}, here are the details for your upcoming rent payment.`,
body: detailTable([
{ label: "Property", value: propertyLabel },
{ label: "Amount due", value: amount, accent: true },
{ label: "Due date", value: dueDate },
]),
button: paymentLink ? { href: paymentLink, label: "Pay rent now" } : undefined,
footerNote: `Sent by ${escapeHtml(senderName)} via ${escapeHtml(APP_NAME)}.`,
})
}