Encode sign-in mail as base64 so webmail cannot mangle the verify token.
Quoted-printable still rewrote token= as token=3D in the raw MIME, which some clients never decode. Distinguish a missing token from a real expiry, and keep links valid for 60 minutes. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
+12
-6
@@ -18,17 +18,22 @@ export function resolveMailFrom(): string {
|
||||
);
|
||||
}
|
||||
|
||||
/** Plaintext + HTML for one-time verify URLs. HTML `href` survives quoted-printable wrapping. */
|
||||
/** Avoid quoted-printable `token=3D` wrapping that some webmail clients do not decode. */
|
||||
export const MAIL_TEXT_ENCODING = "base64" as const;
|
||||
|
||||
/** Plaintext + HTML for one-time verify URLs. */
|
||||
export function buildVerifyLinkParts(
|
||||
verifyUrl: string,
|
||||
intro: string,
|
||||
outro: string,
|
||||
linkLabel: string,
|
||||
): { text: string; html: string } {
|
||||
const text = `${intro}\n\n${verifyUrl}\n\n${outro}`;
|
||||
const href = escapeHtml(verifyUrl);
|
||||
const text = `${intro}\n\n<${verifyUrl}>\n\n${outro}`;
|
||||
const html =
|
||||
`<p>${escapeHtml(intro).replace(/\n/g, "<br />")}</p>` +
|
||||
`<p><a href="${escapeHtml(verifyUrl)}">${escapeHtml(linkLabel)}</a></p>` +
|
||||
`<p><a href="${href}">${escapeHtml(linkLabel)}</a></p>` +
|
||||
`<p><a href="${href}">${href}</a></p>` +
|
||||
`<p>${escapeHtml(outro)}</p>`;
|
||||
return { text, html };
|
||||
}
|
||||
@@ -59,6 +64,7 @@ async function sendHtmlMail(opts: {
|
||||
subject: opts.subject,
|
||||
text: opts.text,
|
||||
html: opts.html,
|
||||
textEncoding: MAIL_TEXT_ENCODING,
|
||||
replyTo: opts.replyTo,
|
||||
});
|
||||
}
|
||||
@@ -69,7 +75,7 @@ export async function sendMagicLinkEmail(
|
||||
): Promise<void> {
|
||||
const { text, html } = buildVerifyLinkParts(
|
||||
verifyUrl,
|
||||
"Open this link to sign in (it expires in 15 minutes):",
|
||||
"Open this link to sign in (it expires in 60 minutes):",
|
||||
"If you did not request this, you can ignore this email.",
|
||||
"Sign in",
|
||||
);
|
||||
@@ -90,7 +96,7 @@ export async function sendRuleStakeholderInviteEmail(
|
||||
): Promise<void> {
|
||||
const { text, html } = buildVerifyLinkParts(
|
||||
verifyUrl,
|
||||
`You've been invited to view "${ruleTitle}" on Community Rule.\n\nOpen this link to create your account (or sign in) and open the rule. The link expires in 15 minutes and works once:`,
|
||||
`You've been invited to view "${ruleTitle}" on Community Rule.\n\nOpen this link to create your account (or sign in) and open the rule. The link expires in 60 minutes and works once:`,
|
||||
"If you did not expect this, you can ignore this email.",
|
||||
"Open the rule",
|
||||
);
|
||||
@@ -134,7 +140,7 @@ export async function sendEmailChangeEmail(
|
||||
): Promise<void> {
|
||||
const { text, html } = buildVerifyLinkParts(
|
||||
verifyUrl,
|
||||
"You asked to change the email on your Community Rule account.\n\nOpen this link to confirm the new address (it expires in 15 minutes):",
|
||||
"You asked to change the email on your Community Rule account.\n\nOpen this link to confirm the new address (it expires in 60 minutes):",
|
||||
"If you did not request this change, you can ignore this email. Your current login is unchanged until you confirm.",
|
||||
"Confirm email",
|
||||
);
|
||||
|
||||
@@ -1,2 +1,2 @@
|
||||
/** Parity with magic-link request TTL (15 minutes). */
|
||||
export const STAKEHOLDER_INVITE_TTL_MS = 15 * 60 * 1000;
|
||||
/** Parity with magic-link request TTL (60 minutes). */
|
||||
export const STAKEHOLDER_INVITE_TTL_MS = 60 * 60 * 1000;
|
||||
|
||||
Reference in New Issue
Block a user