assessment-model/src/app/email_templates/magic_link.ts
Khalim Conn-Kowlessar ee506425fd Drop link from email body; fix paste + stale-resend-notice UX bugs
Email body now contains the 6-digit code only. The /verify/[token] route
and the EmailProvider link callback are intentionally left wired (just
not advertised in the email) so reverting to a link-bearing template is
a content-only change if the all-code variant doesn't improve
deliverability for the Atkins-style blocked recipients.

Hypothesis being tested: corporate gateway URL scanners are part of why
some emails got silently quarantined, and a short transactional body
without an auth-token URL clears more filters.

Two small UX bugs surfaced in preview testing also fixed here:

- Paste of "482 911" (with the visual space from the email's formatted
  code) was dropping a digit. Root cause: maxLength=6 on the input
  truncated the 7-char paste before our \D-stripping ran. Drop the
  attribute and let the existing .slice(0, 6) after stripping handle
  the bound. Pasting the formatted code now works.

- After requesting a resend and then typing into the code field, the
  green "we've sent a new code" notice would re-appear as soon as the
  previous error message cleared. handleChange now clears the resend
  notice on the next keystroke, alongside the error clear it already
  did.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-27 14:46:33 +00:00

132 lines
4.2 KiB
TypeScript
Raw Permalink Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// Email template for user sign-in. Body contains a 6-digit code only — the
// magic-link path is still wired (see /verify/[token] and the EmailProvider
// callback) but the URL is intentionally omitted from the email to reduce
// the content-scanner surface area for corporate email gateways.
import { createTransport } from "nodemailer";
import { buildMailHeaders } from "./buildMailHeaders";
export async function MagicLinksEmail({
identifier,
url,
provider,
code,
}: {
identifier: string;
url: string;
provider: { server: any; from: string };
code: string;
}): Promise<{ messageId: string }> {
const parsed = new URL(url);
const host = parsed.host;
const logoUrl = `${parsed.origin}/domna-email-logo.png`;
const transport = createTransport(provider.server);
const brandColor = "#14163d";
const accentColor = "#2d348f";
const background = "#F9F9F9";
const result = await transport.sendMail({
to: identifier,
from: provider.from,
subject: "Sign in to Ara",
text: plainText({ code, host }),
html: domnaHtml({
code,
logoUrl,
host,
brandColor,
accentColor,
background,
}),
headers: buildMailHeaders({
fromAddress: provider.from,
sesConfigurationSet: process.env.SES_CONFIGURATION_SET,
}),
});
const failed = result.rejected.filter(Boolean);
if (failed.length) {
throw new Error(`Email(s) (${failed.join(", ")}) could not be sent`);
}
return { messageId: result.messageId };
}
function formatCodeForDisplay(code: string): string {
// Insert a non-breaking space for readability without breaking copy-paste
// semantics in mail clients that strip the visual grouping.
return `${code.slice(0, 3)} ${code.slice(3)}`;
}
function domnaHtml({
code,
logoUrl,
host,
brandColor,
accentColor,
background,
}: {
code: string;
logoUrl: string;
host: string;
brandColor: string;
accentColor: string;
background: string;
}) {
const escapedHost = host.replace(/\./g, "&#8203;.");
const codeDisplay = formatCodeForDisplay(code);
return `
<body style="background: ${background}; font-family: Helvetica, Arial, sans-serif; margin: 0; padding: 0;">
<table width="100%" border="0" cellspacing="0" cellpadding="0"
style="max-width: 600px; margin: 40px auto; background: #fff; border-radius: 12px; overflow: hidden; box-shadow: 0 4px 12px rgba(0,0,0,0.05);">
<tr>
<td align="center" style="background: linear-gradient(90deg, ${brandColor}, ${accentColor}); padding: 12px 8px;">
<img
src="${logoUrl}"
alt="Domna Logo"
width="120"
height="auto"
style="margin-bottom: 4px;"
/>
</td>
</tr>
<tr>
<td align="center" style="padding: 28px 24px 28px; color: #333;">
<h2 style="color: ${brandColor}; font-size: 22px; margin: 0 0 8px;">Your sign-in code</h2>
<p style="font-size: 14px; line-height: 1.5; color: #666; margin: 0 0 20px;">
Enter this code at <span style="color: ${accentColor};">${escapedHost}</span> to sign in to Ara.
</p>
<div style="font-family: 'Courier New', Courier, monospace; font-size: 36px; font-weight: 700; letter-spacing: 4px; color: ${brandColor}; padding: 18px 24px; background: #f3f4f8; border-radius: 8px; display: inline-block;">
${codeDisplay}
</div>
<p style="font-size: 12px; color: #888; margin: 12px 0 0;">
This code expires in 10 minutes.
</p>
<p style="margin-top: 28px; font-size: 13px; color: #777;">
If you didnt request this email, you can safely ignore it.
</p>
</td>
</tr>
<tr>
<td align="center" style="padding: 20px; font-size: 12px; color: #999; border-top: 1px solid #eee;">
&copy; ${new Date().getFullYear()} Domna Homes • <span style="color: ${accentColor};">${escapedHost}</span>
</td>
</tr>
</table>
</body>
`;
}
function plainText({ code, host }: { code: string; host: string }) {
return `Sign in to Ara by Domna
Your sign-in code: ${code}
Enter this code at ${host}/auth/verify-code to sign in.
This code expires in 10 minutes. If you did not request this email, you can safely ignore it.
`;
}