addGuests: deduplication keeps the first email but the last guest object

Open Beginner friendly
#29,849 2 comments 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

Assessment

Difficulty
2/5
Estimated time
1-3 hours
Newbie friendliness
72/100
Issue type
Bug
Clarity
Clearly specified
Activity status
Quiet
Tech stack
typescript
Domain
backend

Research direction

Start in packages/trpc/server/routers/viewer/bookings/addGuests.handler.ts and inspect sanitizeAndFilterGuests(), deduplicateGuestEmails(), and emailToGuestMap. Check the call from packages/features/bookings/services/BookingAttendeesService.ts:103. Done means duplicate base emails consistently retain the first guest object, including its name, email, timeZone, and language.

Written by the indexing model from the issue text.

Description

Issue Summary

sanitizeAndFilterGuests() in packages/trpc/server/routers/viewer/bookings/addGuests.handler.ts deduplicates guests by their base email (plus-addressing stripped), but it does that in two places that disagree about which duplicate wins.

deduplicateGuestEmails() keeps the first occurrence:

function deduplicateGuestEmails(guests: string[]): string[] {
  const seenBaseEmails = new Set<string>();
  return guests.filter((guest) => {
    const baseEmail = extractBaseEmail(guest).toLowerCase();
    if (seenBaseEmails.has(baseEmail)) return false;
    seenBaseEmails.add(baseEmail);
    return true;
  });
}

…while emailToGuestMap is built with new Map(...), where a later entry overwrites an earlier one:

const emailToGuestMap = new Map(
  guests.map((guest) => [extractBaseEmail(guest.email).toLowerCase(), guest])
);
...
return uniqueGuestEmails
  .map((email) => emailToGuestMap.get(extractBaseEmail(email).toLowerCase()))

So the surviving email comes from the first duplicate, but the guest object that gets looked up is the last one. When they differ, the wrong person is added.

Actual results

Adding two guests whose base emails collide:

guests = [
  { email: "alice+work@x.com", name: "Alice Work" },
  { email: "alice+home@x.com", name: "Alice Home" },
];

deduplicated  -> ["alice+work@x.com"]                              // first wins
emailToGuestMap -> { "alice@x.com" => { email: "alice+home@x.com", name: "Alice Home" } }  // last wins
result        -> [{ email: "alice+home@x.com", name: "Alice Home" }]

The guest that survives deduplication is alice+work@x.com, but the object returned — and therefore the name, email, timeZone and language written to the booking by addGuestsHandlerupdateBookingAttendees — belongs to alice+home@x.com.

packages/features/bookings/services/BookingAttendeesService.ts:103 calls the same function.

The mismatch also affects the plain-duplicate case: sending the same base email twice with different display names silently keeps the second name while reporting the first address.

Expected results

The guest object that is returned is the one whose email survived deduplication. One consistent tie-break — first occurrence wins, matching deduplicateGuestEmails — used in both places.

Reproduction
const extractBaseEmail = (e) => { const [l, d] = e.split("@"); return `${l.split("+")[0]}@${d}`; };

const guests = [
  { email: "alice+work@x.com", name: "Alice Work" },
  { email: "alice+home@x.com", name: "Alice Home" },
];

const map = new Map(guests.map((g) => [extractBaseEmail(g.email).toLowerCase(), g]));
map.get("alice@x.com");  // { email: 'alice+home@x.com', name: 'Alice Home' }  <- second one
Related

While reading this: extractBaseEmail() in packages/lib/extract-base-email.ts returns the string "noatsign@undefined" for an input with no @, because email.split("@") yields a single element and domain is undefined. That value then flows into the blacklist comparison and the user lookup. It's a separate, smaller issue — happy to file it on its own if you'd prefer.

I have a fix ready and will open a PR referencing this issue.

Dominant language
TypeScript
Stars
48.6k
Forks
15.2k
Avg merge
2d 11h
Merged PRs (30d)
12

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

More from calcom/cal.diy

All issues in calcom/cal.diy

Similar issues

More TypeScript issues

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.