2026-07-02 13:42:34 -04:00
import { getTransporter , emailConfigured , FROM_EMAIL , APP_NAME } from "./client"
import {
BRAND ,
emailShell ,
detailTable ,
statusBadge ,
paragraph ,
escapeHtml ,
htmlToText ,
} from "./layout"
2026-06-23 20:36:07 -04:00
2026-07-02 13:42:34 -04:00
// Re-exported so existing importers (`@/lib/email/send`) keep working.
export { escapeHtml }
2026-06-23 20:36:07 -04:00
interface SendEmailOptions {
to : string
subject : string
html : string
2026-07-02 13:42:34 -04:00
/** Optional plain-text part. Auto-derived from `html` when omitted. */
text? : string
2026-06-23 20:36:07 -04:00
from ?: string
}
2026-07-02 13:42:34 -04:00
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" }
}
2026-06-23 20:36:07 -04:00
2026-07-02 13:42:34 -04:00
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 ) {
2026-06-23 20:36:07 -04:00
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
}) {
2026-07-02 13:42:34 -04:00
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." ,
})
2026-06-23 20:36:07 -04:00
}
export function rentOverdueHtml ({
tenantName ,
propertyName ,
unitNumber ,
amount ,
dueDate ,
} : {
tenantName : string
propertyName : string
unitNumber : string
amount : string
dueDate : string
}) {
2026-07-02 13:42:34 -04:00
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."
),
})
2026-06-23 20:36:07 -04:00
}
export function leaseExpiryHtml ({
tenantName ,
propertyName ,
unitNumber ,
leaseEnd ,
daysLeft ,
} : {
tenantName : string
propertyName : string
unitNumber : string
leaseEnd : string
daysLeft : number
}) {
2026-07-02 13:42:34 -04:00
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." ),
})
2026-06-23 20:36:07 -04:00
}
export function maintenanceUpdateHtml ({
tenantName ,
title ,
status ,
resolutionNotes ,
} : {
tenantName : string
title : string
status : string
resolutionNotes? : string
}) {
2026-07-02 13:42:34 -04:00
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 ) } .` ,
})
2026-06-23 20:36:07 -04:00
}