fix(email): make team org invites more transactional (#732)

* fix(email): make team org invites more transactional

Use org-branded sender and subject instead of the default
"Best from ClassroomIO" marketing identity. Include the inviter
name in the body, remove promotional emojis, and thread optional
subject overrides through the email job pipeline.

Co-authored-by: Rotimi Best <[email protected]>

* fix(email): escape invite fields and sanitize email headers

Address Greptile review: escape inviter/org/role values in the team
invite template, sanitize display names in buildEmailFromName, and
strip control characters from dynamic subjects.

Co-authored-by: Rotimi Best <[email protected]>

* fix(email): use RFC 5322 backslash escaping in display names

Co-authored-by: Rotimi Best <[email protected]>

* test(email): add vitest coverage for email header helpers

Cover escapeHtml, sanitizeEmailDisplayName, sanitizeEmailSubject, and
buildEmailFromName. Collapse repeated whitespace when stripping control
characters from header values.

Co-authored-by: Rotimi Best <[email protected]>

* fix(email): strip all ASCII control chars from email headers

Co-authored-by: Rotimi Best <[email protected]>

---------

Co-authored-by: Cursor Agent <[email protected]>
Co-authored-by: Rotimi Best <[email protected]>
This commit is contained in:
Rotimi Best
2026-07-11 22:22:36 +01:00
committed by GitHub
co-authored by Rotimi Best Cursor Agent
parent 1a9c240501
commit c03c08fc84
12 changed files with 159 additions and 15 deletions
@@ -1,6 +1,36 @@
import { FromData } from '../types';
import { EMAIL_FROM } from '../constants';
const EMAIL_HEADER_CONTROL_CHARS = /[\x00-\x1F\x7F]/g;
function stripEmailHeaderControlChars(value: string): string {
return value.replace(EMAIL_HEADER_CONTROL_CHARS, ' ').replace(/\s+/g, ' ').trim();
}
/** Strip control chars and backslash-escape quotes for RFC 5322 quoted display names. */
export function sanitizeEmailDisplayName(name: string): string {
const normalized = stripEmailHeaderControlChars(name);
if (!normalized) {
return '';
}
return normalized.replace(/\\/g, '\\\\').replace(/"/g, '\\"');
}
/** Strip control chars that could break or inject email headers. */
export function sanitizeEmailSubject(subject: string): string {
return stripEmailHeaderControlChars(subject);
}
export function escapeHtml(value: string): string {
return value
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
.replace(/'/g, '&#39;');
}
// format: "ClassroomIO Developers (via ClassroomIO.com)" <[email protected]>
export function extractNameAndEmail(str: string): FromData | undefined {
// Use regular expressions to match the name and email
@@ -28,5 +58,10 @@ export function buildEmailFromName(name?: string): string {
return EMAIL_FROM;
}
return `"${name}" <${fromData.email}>`;
const displayName = sanitizeEmailDisplayName(name);
if (!displayName) {
return EMAIL_FROM;
}
return `"${displayName}" <${fromData.email}>`;
}