Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 9 additions & 1 deletion components/calendar/event-modal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -418,9 +418,10 @@ export function EventModal({

const timeZone = Intl.DateTimeFormat().resolvedOptions().timeZone;

const trimmedDescription = description.trim();

const data: Partial<CalendarEvent> = {
title: trimmedTitle,
description: description.trim(),
start: startStr,
duration,
timeZone: allDay ? null : timeZone,
Expand All @@ -431,6 +432,13 @@ export function EventModal({
privacy: "public",
};

// On an existing event the empty string is how a description gets cleared, but
// sending it on creation writes a `DESCRIPTION:` with nothing after it — which
// invitation e-mails then render as a "Description" heading over blank space.
if (trimmedDescription || event) {
data.description = trimmedDescription;
}

if (!event) {
data.uid = generateUUID();
}
Expand Down
17 changes: 17 additions & 0 deletions lib/__tests__/calendar-participants.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -361,6 +361,23 @@ describe('buildParticipantMap', () => {
});
});

it('omits blank names rather than sending an empty one', () => {
const map = buildParticipantMap(
{ name: '', email: 'alice@example.com' },
[
{ name: ' ', email: 'bob@example.com' },
{ name: ' Carol ', email: 'carol@example.com' },
]
);
const entries = Object.values(map);

// An empty name becomes a bare `CN=` downstream, which renders as a dangling
// "- Organizer" and as guests listed with nothing before their address.
expect(entries.find(p => p.email === 'alice@example.com')).not.toHaveProperty('name');
expect(entries.find(p => p.email === 'bob@example.com')).not.toHaveProperty('name');
expect(entries.find(p => p.email === 'carol@example.com')!.name).toBe('Carol');
});

it('sets kind to individual for all entries', () => {
const map = buildParticipantMap(
{ name: 'Alice', email: 'alice@example.com' },
Expand Down
9 changes: 7 additions & 2 deletions lib/calendar-participants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -134,12 +134,17 @@ export function buildParticipantMap(

const generateId = () => generateUUID();

// An empty name is not a name: sent as "", it reaches the iCalendar stream as a
// bare `CN=`, and recipients see a dangling "- Organizer" or a guest listed with
// nothing before their address. Omitted, clients fall back to the address.
const named = (name: string) => (name.trim() ? { name: name.trim() } : {});

// calendarAddress is the scheduling address in draft-ietf-calext-jscalendarbis
// (implemented by Stalwart); the RFC 8984 sendTo property is retired there and
// stored as an inert JSPROP, so it is intentionally not sent.
participants[generateId()] = {
'@type': 'Participant',
name: organizer.name,
...named(organizer.name),
email: organizer.email,
calendarAddress: `mailto:${organizer.email}`,
// owner only, NOT attendee: with roles.attendee set, Stalwart's server-side
Expand All @@ -155,7 +160,7 @@ export function buildParticipantMap(
attendees.forEach((a) => {
participants[generateId()] = {
'@type': 'Participant',
name: a.name,
...named(a.name),
email: a.email,
calendarAddress: `mailto:${a.email}`,
roles: { attendee: true },
Expand Down