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:
+23
-13
@@ -1,6 +1,7 @@
|
||||
import { afterEach, describe, expect, it } from "vitest";
|
||||
import nodemailer from "nodemailer";
|
||||
import {
|
||||
MAIL_TEXT_ENCODING,
|
||||
buildVerifyLinkParts,
|
||||
resolveMailFrom,
|
||||
} from "../../lib/server/mail";
|
||||
@@ -8,12 +9,17 @@ import {
|
||||
const VERIFY_URL =
|
||||
"https://staging.communityrule.info/api/auth/magic-link/verify?token=5IdE_BHowaw-QJj7Rwue7CbB8wDXvYITvnxRb1FGqxA";
|
||||
|
||||
function decodeQuotedPrintable(value: string): string {
|
||||
return value
|
||||
.replace(/=\r?\n/g, "")
|
||||
.replace(/=([0-9A-Fa-f]{2})/g, (_, hex: string) =>
|
||||
String.fromCharCode(Number.parseInt(hex, 16)),
|
||||
);
|
||||
function decodeBase64Parts(raw: string): string {
|
||||
const blocks = [
|
||||
...raw.matchAll(
|
||||
/Content-Transfer-Encoding:\s*base64\s*\r?\n\r?\n([A-Za-z0-9+/=\s]+)/gi,
|
||||
),
|
||||
];
|
||||
return blocks
|
||||
.map((match) =>
|
||||
Buffer.from(match[1].replace(/\s/g, ""), "base64").toString("utf8"),
|
||||
)
|
||||
.join("\n");
|
||||
}
|
||||
|
||||
const MAIL_FROM_KEYS = ["SMTP_FROM", "CLOUDRON_MAIL_FROM"] as const;
|
||||
@@ -30,15 +36,16 @@ afterEach(() => {
|
||||
});
|
||||
|
||||
describe("buildVerifyLinkParts", () => {
|
||||
it("puts the exact verify URL in both text and the HTML href", () => {
|
||||
it("puts the exact verify URL in text, href, and visible HTML link", () => {
|
||||
const { text, html } = buildVerifyLinkParts(
|
||||
VERIFY_URL,
|
||||
"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",
|
||||
);
|
||||
expect(text).toContain(VERIFY_URL);
|
||||
expect(text).toContain(`<${VERIFY_URL}>`);
|
||||
expect(html).toContain(`href="${VERIFY_URL}"`);
|
||||
expect(html).toContain(`>${VERIFY_URL}</a>`);
|
||||
expect(html).toContain(">Sign in</a>");
|
||||
});
|
||||
|
||||
@@ -57,10 +64,10 @@ describe("buildVerifyLinkParts", () => {
|
||||
});
|
||||
|
||||
describe("MIME encoding of verify-link mail", () => {
|
||||
it("keeps a clickable href after quoted-printable encoding", async () => {
|
||||
it("uses base64 so the raw MIME never contains token=3D", async () => {
|
||||
const { text, html } = buildVerifyLinkParts(
|
||||
VERIFY_URL,
|
||||
"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",
|
||||
);
|
||||
@@ -75,14 +82,17 @@ describe("MIME encoding of verify-link mail", () => {
|
||||
subject: "Sign in to Community Rule",
|
||||
text,
|
||||
html,
|
||||
textEncoding: MAIL_TEXT_ENCODING,
|
||||
});
|
||||
const raw = Buffer.isBuffer(info.message)
|
||||
? info.message.toString("utf8")
|
||||
: String(info.message);
|
||||
const decoded = decodeQuotedPrintable(raw);
|
||||
expect(raw).toMatch(/Content-Transfer-Encoding:\s*base64/i);
|
||||
expect(raw).not.toContain("token=3D");
|
||||
expect(raw).not.toMatch(/quoted-printable/i);
|
||||
const decoded = decodeBase64Parts(raw);
|
||||
expect(decoded).toContain(`href="${VERIFY_URL}"`);
|
||||
expect(decoded).toContain(VERIFY_URL);
|
||||
expect(decoded).not.toContain("token=3D");
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user