Files
classroomio/packages/email/src/emails/invite-teacher.ts
T
c03c08fc84 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 <rotimi-best@users.noreply.github.com>

* 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 <rotimi-best@users.noreply.github.com>

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

Co-authored-by: Rotimi Best <rotimi-best@users.noreply.github.com>

* 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 <rotimi-best@users.noreply.github.com>

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

Co-authored-by: Rotimi Best <rotimi-best@users.noreply.github.com>

---------

Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Rotimi Best <rotimi-best@users.noreply.github.com>
2026-07-11 22:22:36 +01:00

44 lines
1.5 KiB
TypeScript

import * as z from 'zod';
import { defineEmail } from '../send';
import { getDefaultTemplate } from '../templates';
import { ZEmailBranding } from '../core/branding';
import { escapeHtml } from '../utils/functions/email-helpers';
export const inviteTeacherEmail = defineEmail({
id: 'inviteTeacher',
subject: 'You have been invited to join an organization on ClassroomIO',
schema: z.object({
email: z.string().email(),
orgName: z.string().min(1),
orgSiteName: z.string().min(1),
roleName: z.string().min(1),
inviterName: z.string().min(1).optional(),
expiresAt: z.string().min(1),
inviteLink: z.url(),
branding: ZEmailBranding
}),
render: (fields) => {
const orgName = escapeHtml(fields.orgName);
const roleName = escapeHtml(fields.roleName);
const inviterName = fields.inviterName ? escapeHtml(fields.inviterName) : undefined;
const inviteLink = escapeHtml(fields.inviteLink);
const expiresAt = escapeHtml(fields.expiresAt);
const invitationLine = inviterName
? `<p><strong>${inviterName}</strong> invited you to join <strong>${orgName}</strong> on ClassroomIO as ${roleName}.</p>`
: `<p>You have been invited to join <strong>${orgName}</strong> on ClassroomIO as ${roleName}.</p>`;
const content = `
<p>Hi there,</p>
${invitationLine}
<p>This invite expires on ${expiresAt} (UTC).</p>
<div>
<a class="button" href="${inviteLink}">Accept Invitation</a>
</div>
`;
return getDefaultTemplate(content, fields.branding);
}
});