Skip to content

fix: avoid duplicate emails from the organizer's calendar by setting schedule agent to client#21095

Closed
mitchellderijcke wants to merge 9 commits intocalcom:mainfrom
mitchellderijcke:fix-caldav-emails
Closed

fix: avoid duplicate emails from the organizer's calendar by setting schedule agent to client#21095
mitchellderijcke wants to merge 9 commits intocalcom:mainfrom
mitchellderijcke:fix-caldav-emails

Conversation

@mitchellderijcke
Copy link

@mitchellderijcke mitchellderijcke commented May 3, 2025

What does this PR do?

When an event is scheduled it is inserted into the organizer's calendar. By default most calendars using calDAV will then send their own e-mail notifying the attendees. This conflicts with Cal.com which does this already. By setting SCHEDULE-AGENT=CLIENT in the .ics calDAV object we tell the server to not send these emails.

To make minimal changes, especially because the ics package is used in multiple parts of the application, I forked ics (named ics2) and created a patch for the version Cal.com is using right now (v2.37.0 => v2.37.2). I'm contacting the maintainer of ics to publish this fix on the official ics package.

In the near future it would be a good idea to add additional tests to the codebase where ics is used so we can also confidently move to v3 which more closely aligns with the calDAV spec such as quoting email addresses.

/claim #9485

Visual Demo

Video Demo:

Full demonstration before (Cal.com) and after (local) using Fastmail calDAV:

https://www.youtube.com/watch?v=EE6wjrhxwUU

Image Demo:

Before fix (on official Cal.com):

Screenshot 2025-05-03 at 14 28 12

After fix (on local instance using RIJX Sendgrid):

Screenshot 2025-05-03 at 14 28 20

Mandatory Tasks

  • I have self-reviewed the code.
  • I have updated the developer docs in /docs if this PR makes changes that would require a documentation change. N/A
  • I confirm automated tests are in place that prove my fix is effective or that my feature works.

How should this be tested?

  • Configure a calendar using calDAV (beta). In this case using Fastmail.
  • Schedule an event, preferably using a different email from your account.
  • Observe how only a Cal.com initiated email is sent to the attendee, rather than two.

Checklist

  • I have read the contributing guide
  • My code follows the style guidelines of this project
  • I have checked if my changes generate no new warnings

Summary by mrge

Updated calendar event invites to set SCHEDULE-AGENT=CLIENT in .ics files, preventing duplicate attendee emails from the organizer’s calendar.

  • Dependencies

    • Switched to a patched ics2 package to support the SCHEDULE-AGENT fix.
  • Bug Fixes

    • Ensured only Cal.com sends event emails, avoiding extra notifications from CalDAV servers.

@mitchellderijcke mitchellderijcke requested a review from a team as a code owner May 3, 2025 12:37
@CLAassistant
Copy link

CLAassistant commented May 3, 2025

CLA assistant check
All committers have signed the CLA.

@graphite-app graphite-app bot added the community Created by Linear-GitHub Sync label May 3, 2025
@graphite-app graphite-app bot requested a review from a team May 3, 2025 12:37
@vercel
Copy link

vercel bot commented May 3, 2025

@rijx is attempting to deploy a commit to the cal Team on Vercel.

A member of the Team first needs to authorize it.

@github-actions github-actions bot added $500 caldav area: caldav, fastmail, Baïkal, Kerio, mailbox, nextcloud Low priority Created by Linear-GitHub Sync 🐛 bug Something isn't working 💎 Bounty A bounty on Algora.io labels May 3, 2025
@dosubot dosubot bot added the emails area: emails, cancellation email, reschedule email, inbox, spam folder, not getting email label May 3, 2025
@graphite-app
Copy link

graphite-app bot commented May 3, 2025

Graphite Automations

"Add consumer team as reviewer" took an action on this PR • (05/03/25)

1 reviewer was added to this PR based on Keith Williams's automation.

"Add community label" took an action on this PR • (05/03/25)

1 label was added to this PR based on Keith Williams's automation.

Copy link
Contributor

@cubic-dev-ai cubic-dev-ai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

mrge found 1 issue across 3 files. View it in mrge.io

@mitchellderijcke mitchellderijcke changed the title fix: ics should set schedule agent to client to avoid emails from the organizer's calendar fix: avoid duplicate emails from the organizer's calendar by setting schedule agent to client May 3, 2025
Copy link
Contributor

@TusharBhatt1 TusharBhatt1 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM , is using ics2 mandatory - can we do it with ics ?

@mitchellderijcke
Copy link
Author

mitchellderijcke commented May 8, 2025

LGTM , is using ics2 mandatory - can we do it with ics ?

Unfortunately I had to fork ics to

A) get schedule agent support (Open PR, no replies yet: adamgibbons/ics#289)
B) avoid scope creep as modern versions (ics v3 and alternatives) interpret the spec slightly different (e.g. properly quoting email addresses) which I think warrants a discussion as ics is used in different places so even when v3 supports the parameter, we can't blindly upgrade the package version as we're on v2.37.0 now.

I also opened a PR for ical-generator which is a potential alternative, but even though the PR has been merged, it's for v9 which is not stable yet: sebbo2002/ical-generator#654

Considering how simple the files actually are (you can check the test, it contains the expected file contents) it wouldn't actually be crazy to integrate the code considering that it's a core functionality and we would benefit from having complete control.

Happy to implement any of the alternatives if there's a preference for one.

@mitchellderijcke
Copy link
Author

Considering how simple the files actually are (you can check the test, it contains the expected file contents) it wouldn't actually be crazy to integrate the code considering that it's a core functionality and we would benefit from having complete control.

I implemented this template and I'm actually rather pleased by its simplicity now that there's no abstraction object between CalendarEvent and the ics library as it uses data directly from the event in its native format.

https://github.com/calcom/cal.com/compare/main...mitchellderijcke:cal.com:fix-caldav-emails-template?expand=1

So that might actually be the way to go, at least until we decide to whether to migrate other instances of ics.

@github-actions
Copy link
Contributor

This PR is being marked as stale due to inactivity.

@github-actions github-actions bot added the Stale label May 24, 2025
@github-actions github-actions bot removed the Stale label May 25, 2025
@github-actions
Copy link
Contributor

This PR is being marked as stale due to inactivity.

@github-actions github-actions bot added the Stale label Jun 28, 2025
@coderabbitai
Copy link
Contributor

coderabbitai bot commented Aug 28, 2025

Note

Other AI code review bot(s) detected

CodeRabbit has detected other AI code review bot(s) in this pull request and will avoid duplicating their findings in the review comments. This may lead to a less comprehensive review.

Walkthrough

The PR switches ICS generation from the ics package to ics2 in CalendarService and updates attendee mapping to include rsvp: false and scheduleAgent: "CLIENT". It adds ics2 as a dependency. A new test suite (CalendarService.test.ts) mocks crypto, tsdav, uuid, and parsing utilities to validate ICS input creation and CalDAV create/update flows. Tests check that createEvent is called with expected inputs and that createCalendarObject/updateCalendarObject receive correct payloads, including deterministic UID handling in tests and verification of returned event object structures.

Assessment against linked issues

Objective Addressed Explanation
Set SCHEDULE-AGENT=CLIENT on invitations (#9485)
Ensure a single, consistent UID per event across invitations/updates (#9485) No explicit change ensuring UID stability across all notifications; tests use a mocked fixed UUID only.
Include timezone information in event times (#9485) No changes related to timezone fields or TZID in ICS generation observed.
Provide option to disable calendar event creation (#9485) No configuration or code path to skip event creation added.

Tip

🔌 Remote MCP (Model Context Protocol) integration is now available!

Pro plan users can now connect to remote MCP servers from the Integrations page. Connect with popular remote MCPs such as Notion and Linear to add more context to your reviews and chats.

✨ Finishing Touches
  • 📝 Generate Docstrings
🧪 Generate unit tests
  • Create PR with unit tests
  • Post copyable unit tests in a comment

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share
🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>, please review it.
    • Open a follow-up GitHub issue for this discussion.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbit in a new review comment at the desired location with your query.
  • PR comments: Tag @coderabbit in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbit gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbit read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.

Support

Need help? Create a ticket on our support page for assistance with any issues or questions.

CodeRabbit Commands (Invoked using PR/Issue comments)

Type @coderabbit help to get the list of available commands.

Other keywords and placeholders

  • Add @coderabbit ignore or @coderabbitai ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbit summary or @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbit or @coderabbitai anywhere in the PR title to generate the title automatically.

Status, Documentation and Community

  • Visit our Status Page to check the current availability of CodeRabbit.
  • Visit our Documentation for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
packages/lib/CalendarService.ts (2)

273-279: Bug: wrong event type returned in updateEvent.

this.credentials does not have a type field; should mirror createEvent and use this.integrationName.

Fix:

-              type: this.credentials.type,
+              type: this.integrationName,

703-709: Incorrect object URL when fetching by UID (missing slash).

${cal.externalId}${uid}.ics will produce …/calendar.ics for bases without trailing slash.

Patch:

-      const calEvents = await this.getEvents(cal.externalId, null, null, [`${cal.externalId}${uid}.ics`]);
+      const base = cal.externalId.replace(/\/$/, "");
+      const calEvents = await this.getEvents(cal.externalId, null, null, [`${base}/${uid}.ics`]);

Optionally update tests/mocks to reflect the object URL shape (…/calendar/.ics).

🧹 Nitpick comments (4)
packages/lib/CalendarService.ts (1)

136-147: De-duplicate attendees by email to avoid repeated ATTENDEE lines.

If an attendee is both in event.attendees and team.members, duplicates will be emitted.

Apply:

   private getAttendees(event: CalendarEvent) {
-    const attendees = mapAttendees(event.attendees);
-
-    if (event.team?.members) {
-      const teamAttendeesWithoutCurrentUser = event.team.members.filter(
-        (member) => member.email !== this.credential.user?.email
-      );
-      attendees.push(...mapAttendees(teamAttendeesWithoutCurrentUser));
-    }
-
-    return attendees;
+    const combined =
+      [
+        ...event.attendees,
+        ...(event.team?.members?.filter((m) => m.email !== this.credential.user?.email) ?? []),
+      ] as (AttendeeInCalendarEvent | TeamMember)[];
+
+    const seen = new Set<string>();
+    const deduped = combined.filter((a) => {
+      const key = (a.email ?? "").toLowerCase();
+      if (!key || seen.has(key)) return false;
+      seen.add(key);
+      return true;
+    });
+
+    return mapAttendees(deduped);
   }
packages/lib/CalendarService.test.ts (3)

65-87: Brittle full-ICS snapshot; assert essentials instead.

Full string snapshots (PRODID, folding, header order) are upgrade-fragile. Prefer asserting critical fields (UID, DTSTART, DURATION, ORGANIZER, and SCHEDULE-AGENT on ATTENDEE).

Example:

-  iCalString: getExpectedICS({ title: "Test Event" }),
+  iCalString: expect.stringContaining("SCHEDULE-AGENT=CLIENT"),

And separately assert UID/DTSTART/DURATION with targeted matchers.


177-186: Also assert METHOD is stripped before upload (RFC 4791).

You already strip METHOD in code; add an explicit test.

Apply:

     expect(createCalendarObject).toHaveBeenCalledWith({
       calendar: {
         url: "https://caldevtest/calendar",
       },
       headers: {
         authorization: "MOCK",
       },
       filename: "00000000-0000-0000-0000-000000000001.ics",
       iCalString: getExpectedICS({ title: "Test Event" }),
     });
+
+    const uploaded = vi.mocked(createCalendarObject).mock.calls[0][0].iCalString as string;
+    expect(uploaded).not.toMatch(/METHOD:/);

225-234: Mirror METHOD stripping assertion on update.

Ensure updates also upload without METHOD.

Apply:

     expect(updateCalendarObject).toHaveBeenCalledWith({
       calendarObject: {
         data: getExpectedICS({ title: "NEW TITLE" }),
         etag: "testetag",
         url: "https://caldevtest/calendar",
       },
       headers: {
         authorization: "MOCK",
       },
     });
+
+    const updated = vi.mocked(updateCalendarObject).mock.calls[0][0].calendarObject.data as string;
+    expect(updated).not.toMatch(/METHOD:/);
📜 Review details

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 59c7614 and c8dfe54.

⛔ Files ignored due to path filters (1)
  • yarn.lock is excluded by !**/yarn.lock, !**/*.lock
📒 Files selected for processing (3)
  • packages/lib/CalendarService.test.ts (1 hunks)
  • packages/lib/CalendarService.ts (2 hunks)
  • packages/lib/package.json (1 hunks)
🧰 Additional context used
📓 Path-based instructions (4)
**/*.ts

📄 CodeRabbit inference engine (.cursor/rules/review.mdc)

**/*.ts: For Prisma queries, only select data you need; never use include, always use select
Ensure the credential.key field is never returned from tRPC endpoints or APIs

Files:

  • packages/lib/CalendarService.test.ts
  • packages/lib/CalendarService.ts
**/*.{ts,tsx}

📄 CodeRabbit inference engine (.cursor/rules/review.mdc)

Flag excessive Day.js use in performance-critical code; prefer native Date or Day.js .utc() in hot paths like loops

Files:

  • packages/lib/CalendarService.test.ts
  • packages/lib/CalendarService.ts
**/*.{ts,tsx,js,jsx}

⚙️ CodeRabbit configuration file

Flag default exports and encourage named exports. Named exports provide better tree-shaking, easier refactoring, and clearer imports. Exempt main components like pages, layouts, and components that serve as the primary export of a module.

Files:

  • packages/lib/CalendarService.test.ts
  • packages/lib/CalendarService.ts
**/*Service.ts

📄 CodeRabbit inference engine (.cursor/rules/review.mdc)

Service files must include Service suffix, use PascalCase matching exported class, and avoid generic names (e.g., MembershipService.ts)

Files:

  • packages/lib/CalendarService.ts
🧠 Learnings (3)
📓 Common learnings
Learnt from: anglerfishlyy
PR: calcom/cal.com#0
File: :0-0
Timestamp: 2025-08-27T16:39:38.156Z
Learning: anglerfishlyy successfully implemented CAL-3076 email invitation feature for Cal.com team event-types in PR #23312. The feature allows inviting people via email directly from assignment flow, with automatic team invitation if email doesn't belong to existing team member. Implementation includes Host type modifications (userId?: number, email?: string, isPending?: boolean), CheckedTeamSelect component updates with CreatableSelect, TRPC schema validation with zod email validation, and integration with existing teamInvite system.
Learnt from: din-prajapati
PR: calcom/cal.com#21854
File: packages/app-store/office365calendar/__tests__/unit_tests/SubscriptionManager.test.ts:0-0
Timestamp: 2025-08-05T12:04:29.037Z
Learning: In packages/app-store/office365calendar/lib/CalendarService.ts, the fetcher method in Office365CalendarService class is public, not private. It was specifically changed from private to public in this PR to support proper testing and external access patterns.
📚 Learning: 2025-08-05T12:04:29.037Z
Learnt from: din-prajapati
PR: calcom/cal.com#21854
File: packages/app-store/office365calendar/__tests__/unit_tests/SubscriptionManager.test.ts:0-0
Timestamp: 2025-08-05T12:04:29.037Z
Learning: In packages/app-store/office365calendar/lib/CalendarService.ts, the fetcher method in Office365CalendarService class is public, not private. It was specifically changed from private to public in this PR to support proper testing and external access patterns.

Applied to files:

  • packages/lib/CalendarService.test.ts
  • packages/lib/CalendarService.ts
📚 Learning: 2025-07-18T17:57:16.395Z
Learnt from: vijayraghav-io
PR: calcom/cal.com#21072
File: packages/app-store/office365calendar/api/webhook.ts:120-123
Timestamp: 2025-07-18T17:57:16.395Z
Learning: The office365calendar webhook handler in packages/app-store/office365calendar/api/webhook.ts is specifically designed for Office365 calendar integration, not as a generic webhook handler. Therefore, it's safe to assume that fetchAvailabilityAndSetCache method will be implemented in the Office365CalendarService, making explicit validation checks unnecessary.

Applied to files:

  • packages/lib/CalendarService.test.ts
🧬 Code graph analysis (1)
packages/lib/CalendarService.test.ts (1)
packages/types/Calendar.d.ts (1)
  • Person (33-45)
🔇 Additional comments (4)
packages/lib/CalendarService.ts (2)

102-108: SCHEDULE-AGENT=CLIENT set on attendees — LGTM.

This should stop provider-initiated attendee mail. RSVP=FALSE + PARTSTAT default also looks correct.


157-160: Timezone still serialized as UTC; confirm if local TZ requirements are satisfied.

Issue #9485 calls out including timezone info. Here we force UTC (startInputType: "utc"). If providers/users expect TZID/VTIMEZONE for local rendering, consider emitting local time with TZID or including VTIMEZONE.

Would you like a follow-up patch to add TZID/VTIMEZONE while preserving server-side METHOD stripping?

packages/lib/CalendarService.test.ts (2)

54-61: Stable DTSTAMP extraction — LGTM.

Good approach to avoid flakiness by deriving the library’s DTSTAMP at runtime.


199-205: Mocked object URL shape may hide a bug.

Real CalDAV object URLs are typically …/calendar/.ics. The mock uses the collection URL only, which would mask the missing “/” bug in getEventsByUID. Align the mock with the expected object URL.

Apply (after fixing code):

-        url: "https://caldevtest/calendar",
+        url: "https://caldevtest/calendar/00000000-0000-0000-0000-000000000001.ics",

Comment on lines +5 to +6
import type { Attendee, DateArray, DurationObject } from "ics2";
import { createEvent } from "ics2";
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Verification agent

🧩 Analysis chain

Switch to ics2 acknowledged; verify no lingering ics imports.

Good move to ics2 for SCHEDULE-AGENT support. Please verify nothing else still imports "ics".

Run:


🏁 Script executed:

#!/bin/bash
# Find any remaining imports/usages of the old 'ics' package
rg -nP --glob '!**/node_modules/**' '\bfrom\s+[\"\']ics[\"\']|\brequire\(\s*[\"\']ics[\"\']\s*\)'

Length of output: 116


🏁 Script executed:

#!/bin/bash
# Re-run search for any remaining imports or requires of 'ics' with correct quoting
rg -nP --glob '!**/node_modules/**' "from\s+['\"]ics['\"]|require\(\s*['\"]ics['\"]\s*\)"

Length of output: 861


Replace all remaining ics imports with ics2

The following files still import from "ics" and need their import paths updated to "ics2":

  • packages/lib/bookings/getCalendarLinks.ts (line 3)
  • packages/lib/bookings/getCalendarLinks.test.ts (line 2)
  • packages/features/ee/workflows/lib/getiCalEventAsString.ts (lines 1–2)
  • packages/features/ee/workflows/lib/reminders/emailReminderManager.ts (line 1)
  • packages/emails/lib/generateIcsString.ts (lines 2–3)
  • packages/emails/lib/generateIcsFile.ts (line 2)
    Ensure all imports and type references switch from ics to ics2.
🤖 Prompt for AI Agents
In packages/lib/CalendarService.ts around lines 5–6 and in the other files
listed (packages/lib/bookings/getCalendarLinks.ts line 3,
packages/lib/bookings/getCalendarLinks.test.ts line 2,
packages/features/ee/workflows/lib/getiCalEventAsString.ts lines 1–2,
packages/features/ee/workflows/lib/reminders/emailReminderManager.ts line 1,
packages/emails/lib/generateIcsString.ts lines 2–3,
packages/emails/lib/generateIcsFile.ts line 2), replace any import statements
and type references that import from "ics" to import from "ics2" (e.g.,
import/createEvent/type imports), ensuring both value imports and "import type"
usages are updated consistently so all modules reference "ics2".

"i18next": "^23.2.3",
"ical.js": "^1.4.0",
"ics": "^2.37.0",
"ics2": "^2.37.2",
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Verification agent

🧩 Analysis chain

Add ics2 — pin exact version and consider removing ics if unused.

To avoid unintentional upgrades on a forked package and reduce duplicate deps:

  • Pin exactly: "ics2": "2.37.2".
  • Remove "ics" if not referenced elsewhere.

Script to check usage:


🏁 Script executed:

#!/bin/bash
# Verify 'ics' is unused; list any imports
rg -nP --glob '!**/node_modules/**' '\bfrom\s+[\"\']ics[\"\']|\brequire\(\s*[\"\']ics[\"\']\s*\)'

Length of output: 116


🏁 Script executed:

#!/bin/bash
# Check if 'ics' is listed in dependencies
grep -n '"ics"' packages/lib/package.json || echo "'ics' not found in package.json"
# Search code for imports/requires of 'ics'
rg -nP --glob '!**/node_modules/**' "\\bfrom\\s+['\\\"]ics['\\\"]|\\brequire\\(\\s*['\\\"]ics['\\\"]\\s*\\)" || echo "No imports of 'ics' found in code"

Length of output: 938


Pin ics2 to an exact version
In packages/lib/package.json, change the dependency from

"ics2": "^2.37.2",

to

"ics2": "2.37.2"

The suggestion to remove ics can be dropped—ics is imported in multiple modules (e.g. getiCalEventAsString.ts, emailReminderManager.ts, getCalendarLinks.ts, generateIcsString.ts).

🤖 Prompt for AI Agents
In packages/lib/package.json at line 24, the dependency "ics2" is using a caret
range "^2.37.2" and should be pinned to the exact version; update the
package.json dependency entry to use "2.37.2" (remove the caret) and run
npm/yarn install or update lockfile to persist the change.

Copy link
Contributor

@Devanshusharma2005 Devanshusharma2005 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could you please address the failing tests ?

@pallava-joshi
Copy link
Member

tests are still failing

@pallava-joshi
Copy link
Member

Closing this PR due to inactivity. If you’d like to revisit this in the future, please feel free to open a new one, we’d be happy to take another look.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

🙋 Bounty claim 💎 Bounty A bounty on Algora.io 🐛 bug Something isn't working caldav area: caldav, fastmail, Baïkal, Kerio, mailbox, nextcloud community Created by Linear-GitHub Sync emails area: emails, cancellation email, reschedule email, inbox, spam folder, not getting email Low priority Created by Linear-GitHub Sync size/L Stale $500

Projects

None yet

Development

Successfully merging this pull request may close these issues.

CalDAV integration with Fastmail is generating duplicate, erroneous invitation emails.

5 participants