Skip to content

Commit 197c820

Browse files
fix: prevent HTML encoding in names
1 parent 442779d commit 197c820

12 files changed

+417
-12
lines changed
Lines changed: 134 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,134 @@
1+
import { describe, expect, it } from "vitest";
2+
import { renderToString } from "react-dom/server";
3+
4+
import { buildCalendarEvent, buildPerson } from "@calcom/lib/test/builder";
5+
6+
import { AttendeeRequestEmail } from "./AttendeeRequestEmail";
7+
8+
describe("AttendeeRequestEmail", () => {
9+
it("should not HTML-encode forward slash in organizer name", () => {
10+
const organizerWithSlash = buildPerson({
11+
name: "Carina / Test",
12+
13+
});
14+
15+
const attendee = buildPerson({
16+
name: "John Doe",
17+
18+
});
19+
20+
const calEvent = buildCalendarEvent({
21+
organizer: organizerWithSlash,
22+
attendees: [attendee],
23+
recurringEvent: undefined,
24+
});
25+
26+
const emailComponent = AttendeeRequestEmail({
27+
calEvent,
28+
attendee,
29+
});
30+
31+
const html = renderToString(emailComponent);
32+
33+
// Should contain the raw forward slash, not HTML encoded
34+
expect(html).toContain("Carina / Test");
35+
// Should NOT contain HTML-encoded version
36+
expect(html).not.toContain("/");
37+
expect(html).not.toContain("/");
38+
});
39+
40+
it("should not HTML-encode ampersand in organizer name", () => {
41+
const organizerWithAmpersand = buildPerson({
42+
name: "Smith & Jones",
43+
44+
});
45+
46+
const attendee = buildPerson({
47+
name: "John Doe",
48+
49+
});
50+
51+
const calEvent = buildCalendarEvent({
52+
organizer: organizerWithAmpersand,
53+
attendees: [attendee],
54+
recurringEvent: undefined,
55+
});
56+
57+
const emailComponent = AttendeeRequestEmail({
58+
calEvent,
59+
attendee,
60+
});
61+
62+
const html = renderToString(emailComponent);
63+
64+
// Should contain the raw ampersand, not HTML encoded
65+
expect(html).toContain("Smith & Jones");
66+
// Should NOT contain HTML-encoded version
67+
expect(html).not.toContain("&");
68+
});
69+
70+
it("should not HTML-encode single quote in organizer name", () => {
71+
const organizerWithQuote = buildPerson({
72+
name: "O'Brien",
73+
74+
});
75+
76+
const attendee = buildPerson({
77+
name: "John Doe",
78+
79+
});
80+
81+
const calEvent = buildCalendarEvent({
82+
organizer: organizerWithQuote,
83+
attendees: [attendee],
84+
recurringEvent: undefined,
85+
});
86+
87+
const emailComponent = AttendeeRequestEmail({
88+
calEvent,
89+
attendee,
90+
});
91+
92+
const html = renderToString(emailComponent);
93+
94+
// Should contain the raw single quote, not HTML encoded
95+
expect(html).toContain("O'Brien");
96+
// Should NOT contain HTML-encoded version
97+
expect(html).not.toContain("'");
98+
expect(html).not.toContain("'");
99+
});
100+
101+
it("should handle recurring events with special characters in organizer name", () => {
102+
const organizerWithSlash = buildPerson({
103+
name: "Test / User",
104+
105+
});
106+
107+
const attendee = buildPerson({
108+
name: "John Doe",
109+
110+
});
111+
112+
const calEvent = buildCalendarEvent({
113+
organizer: organizerWithSlash,
114+
attendees: [attendee],
115+
recurringEvent: {
116+
freq: 2, // WEEKLY
117+
count: 5,
118+
interval: 1,
119+
},
120+
});
121+
122+
const emailComponent = AttendeeRequestEmail({
123+
calEvent,
124+
attendee,
125+
});
126+
127+
const html = renderToString(emailComponent);
128+
129+
// Should contain the raw forward slash
130+
expect(html).toContain("Test / User");
131+
// Should NOT contain HTML-encoded version
132+
expect(html).not.toContain("/");
133+
});
134+
});

packages/emails/src/templates/AttendeeRequestEmail.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ export const AttendeeRequestEmail = (props: React.ComponentProps<typeof Attendee
1515
props.calEvent.recurringEvent?.count
1616
? "user_needs_to_confirm_or_reject_booking_recurring"
1717
: "user_needs_to_confirm_or_reject_booking",
18-
{ user: props.calEvent.organizer.name }
18+
{ user: props.calEvent.organizer.name, interpolation: { escapeValue: false } }
1919
)}
2020
</>
2121
}
Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
import { describe, expect, it } from "vitest";
2+
import { renderToString } from "react-dom/server";
3+
4+
import { buildCalendarEvent, buildPerson } from "@calcom/lib/test/builder";
5+
6+
import { AttendeeWasRequestedToRescheduleEmail } from "./AttendeeWasRequestedToRescheduleEmail";
7+
8+
describe("AttendeeWasRequestedToRescheduleEmail", () => {
9+
it("should not HTML-encode forward slash in organizer name", () => {
10+
const organizerWithSlash = buildPerson({
11+
name: "Carina / Test",
12+
13+
});
14+
15+
const attendee = buildPerson({
16+
name: "John Doe",
17+
18+
});
19+
20+
const calEvent = buildCalendarEvent({
21+
organizer: organizerWithSlash,
22+
attendees: [attendee],
23+
});
24+
25+
const emailComponent = AttendeeWasRequestedToRescheduleEmail({
26+
calEvent,
27+
attendee,
28+
metadata: {
29+
rescheduleLink: "https://cal.com/reschedule/abc123",
30+
},
31+
});
32+
33+
const html = renderToString(emailComponent);
34+
35+
// Should contain the raw forward slash, not HTML encoded
36+
expect(html).toContain("Carina / Test");
37+
// Should NOT contain HTML-encoded version
38+
expect(html).not.toContain("&#x2F;");
39+
expect(html).not.toContain("&#47;");
40+
});
41+
42+
it("should not HTML-encode ampersand in organizer name", () => {
43+
const organizerWithAmpersand = buildPerson({
44+
name: "Smith & Jones",
45+
46+
});
47+
48+
const attendee = buildPerson({
49+
name: "John Doe",
50+
51+
});
52+
53+
const calEvent = buildCalendarEvent({
54+
organizer: organizerWithAmpersand,
55+
attendees: [attendee],
56+
});
57+
58+
const emailComponent = AttendeeWasRequestedToRescheduleEmail({
59+
calEvent,
60+
attendee,
61+
metadata: {
62+
rescheduleLink: "https://cal.com/reschedule/abc123",
63+
},
64+
});
65+
66+
const html = renderToString(emailComponent);
67+
68+
// Should contain the raw ampersand, not HTML encoded
69+
expect(html).toContain("Smith & Jones");
70+
// Should NOT contain HTML-encoded version
71+
expect(html).not.toContain("&amp;");
72+
});
73+
74+
it("should not HTML-encode single quote in organizer name", () => {
75+
const organizerWithQuote = buildPerson({
76+
name: "O'Brien",
77+
78+
});
79+
80+
const attendee = buildPerson({
81+
name: "John Doe",
82+
83+
});
84+
85+
const calEvent = buildCalendarEvent({
86+
organizer: organizerWithQuote,
87+
attendees: [attendee],
88+
});
89+
90+
const emailComponent = AttendeeWasRequestedToRescheduleEmail({
91+
calEvent,
92+
attendee,
93+
metadata: {
94+
rescheduleLink: "https://cal.com/reschedule/abc123",
95+
},
96+
});
97+
98+
const html = renderToString(emailComponent);
99+
100+
// Should contain the raw single quote, not HTML encoded
101+
expect(html).toContain("O'Brien");
102+
// Should NOT contain HTML-encoded version
103+
expect(html).not.toContain("&#x27;");
104+
expect(html).not.toContain("&#39;");
105+
});
106+
});

packages/emails/src/templates/AttendeeWasRequestedToRescheduleEmail.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ export const AttendeeWasRequestedToRescheduleEmail = (
1313
<>
1414
{t("request_reschedule_subtitle", {
1515
organizer: props.calEvent.organizer.name,
16+
interpolation: { escapeValue: false },
1617
})}
1718
</>
1819
}

packages/emails/src/templates/BaseScheduledEmail.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@ export const BaseScheduledEmail = (
4848
date: `${getRecipientStart("h:mma")} - ${getRecipientEnd("h:mma")}, ${t(
4949
getRecipientStart("dddd").toLowerCase()
5050
)}, ${t(getRecipientStart("MMMM").toLowerCase())} ${getRecipientStart("D, YYYY")}`,
51+
interpolation: { escapeValue: false },
5152
});
5253

5354
let rescheduledBy = props.calEvent.rescheduledBy;

packages/emails/src/templates/CreditBalanceLimitReachedEmail.tsx

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -26,15 +26,15 @@ export const CreditBalanceLimitReachedEmail = (
2626

2727
if (team) {
2828
return (
29-
<V2BaseEmailHtml subject={user.t("action_required_out_of_credits", { teamName: team.name })}>
29+
<V2BaseEmailHtml subject={user.t("action_required_out_of_credits", { teamName: team.name, interpolation: { escapeValue: false } })}>
3030
<p style={{ fontWeight: 400, lineHeight: "24px" }}>
31-
<> {user.t("hi_user_name", { name: user.name })},</>
31+
<> {user.t("hi_user_name", { name: user.name, interpolation: { escapeValue: false } })},</>
3232
</p>
3333
<p style={{ fontWeight: 400, lineHeight: "24px", marginBottom: "20px" }}>
3434
<>
3535
{isCalAi
36-
? user.t("cal_ai_credit_limit_reached_message", { teamName: team.name })
37-
: user.t("credit_limit_reached_message", { teamName: team.name })}
36+
? user.t("cal_ai_credit_limit_reached_message", { teamName: team.name, interpolation: { escapeValue: false } })
37+
: user.t("credit_limit_reached_message", { teamName: team.name, interpolation: { escapeValue: false } })}
3838
</>
3939
</p>
4040
<div style={{ textAlign: "center", marginTop: "24px" }}>
@@ -51,7 +51,7 @@ export const CreditBalanceLimitReachedEmail = (
5151
return (
5252
<V2BaseEmailHtml subject={user.t("action_required_user_out_of_credits")}>
5353
<p style={{ fontWeight: 400, lineHeight: "24px" }}>
54-
<> {user.t("hi_user_name", { name: user.name })},</>
54+
<> {user.t("hi_user_name", { name: user.name, interpolation: { escapeValue: false } })},</>
5555
</p>
5656
<p style={{ fontWeight: 400, lineHeight: "24px", marginBottom: "20px" }}>
5757
<>

packages/emails/src/templates/CreditBalanceLowWarningEmail.tsx

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -27,15 +27,15 @@ export const CreditBalanceLowWarningEmail = (
2727

2828
if (team) {
2929
return (
30-
<V2BaseEmailHtml subject={user.t("team_credits_low_warning", { teamName: team.name })}>
30+
<V2BaseEmailHtml subject={user.t("team_credits_low_warning", { teamName: team.name, interpolation: { escapeValue: false } })}>
3131
<p style={{ fontWeight: 400, lineHeight: "24px" }}>
32-
<> {user.t("hi_user_name", { name: user.name })},</>
32+
<> {user.t("hi_user_name", { name: user.name, interpolation: { escapeValue: false } })},</>
3333
</p>
3434
<p style={{ fontWeight: 400, lineHeight: "24px", marginBottom: "20px" }}>
3535
<>
3636
{isCalAi
37-
? user.t("cal_ai_low_credits_warning_message", { teamName: team.name })
38-
: user.t("low_credits_warning_message", { teamName: team.name })}
37+
? user.t("cal_ai_low_credits_warning_message", { teamName: team.name, interpolation: { escapeValue: false } })
38+
: user.t("low_credits_warning_message", { teamName: team.name, interpolation: { escapeValue: false } })}
3939
</>
4040
</p>
4141
<p
@@ -60,7 +60,7 @@ export const CreditBalanceLowWarningEmail = (
6060
return (
6161
<V2BaseEmailHtml subject={user.t("user_credits_low_warning")}>
6262
<p style={{ fontWeight: 400, lineHeight: "24px" }}>
63-
<> {user.t("hi_user_name", { name: user.name })},</>
63+
<> {user.t("hi_user_name", { name: user.name, interpolation: { escapeValue: false } })},</>
6464
</p>
6565
<p style={{ fontWeight: 400, lineHeight: "24px", marginBottom: "20px" }}>
6666
<>

packages/emails/src/templates/OrganizerPaymentRefundFailedEmail.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ export const OrganizerPaymentRefundFailedEmail = (
1616
<>
1717
{t("check_with_provider_and_user", {
1818
user: props.calEvent.attendees[0].name,
19+
interpolation: { escapeValue: false },
1920
})}
2021
</>
2122
}>

0 commit comments

Comments
 (0)