Skip to content
Open
Show file tree
Hide file tree
Changes from 10 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
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
3 changes: 2 additions & 1 deletion src/extensions/mail-ext-calendar/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,8 @@
"id": "day-view",
"title": "Calendar",
"priority": 100,
"scope": "email"
"scope": "email",
"ownHeader": true
}
],
"settings": [
Expand Down
69 changes: 66 additions & 3 deletions src/extensions/mail-ext-calendar/src/google-calendar-client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { readFile, readdir } from "fs/promises";
import { existsSync } from "fs";
import { join } from "path";
import { getDataDir } from "../../../main/data-dir";
import type { CalendarEventInsertParams } from "../../../shared/types";

export interface CalendarEvent {
id: string;
Expand All @@ -26,6 +27,10 @@ export interface CalendarInfo {
id: string;
name: string;
color: string;
timezone?: string;
primary?: boolean;
accessRole?: string;
writable: boolean;
}

/** Result of an incremental or full sync for a single calendar. */
Expand Down Expand Up @@ -92,14 +97,41 @@ async function getOAuth2Client(accountId: string): Promise<OAuth2Client | null>
}
}

function tokenScopes(auth: OAuth2Client): Set<string> {
const scopes = auth.credentials.scope || "";
return new Set(scopes.split(/\s+/).filter(Boolean));
}

const CALENDAR_READ_SCOPES = new Set([
"https://www.googleapis.com/auth/calendar.readonly",
"https://www.googleapis.com/auth/calendar.events.readonly",
"https://www.googleapis.com/auth/calendar.events",
"https://www.googleapis.com/auth/calendar",
]);

const CALENDAR_WRITE_SCOPES = new Set([
"https://www.googleapis.com/auth/calendar.events",
"https://www.googleapis.com/auth/calendar",
]);

/**
* Check if the current tokens include calendar scope.
* Check if the current tokens include any calendar read scope.
*/
export async function hasCalendarScope(accountId: string): Promise<boolean> {
const auth = await getOAuth2Client(accountId);
if (!auth) return false;
const scopes = auth.credentials.scope || "";
return scopes.includes("calendar.readonly") || scopes.includes("calendar");
const scopes = tokenScopes(auth);
return Array.from(CALENDAR_READ_SCOPES).some((scope) => scopes.has(scope));
}

/**
* Check if the current tokens include Google Calendar event write scope.
*/
export async function hasCalendarWriteScope(accountId: string): Promise<boolean> {
const auth = await getOAuth2Client(accountId);
if (!auth) return false;
const scopes = tokenScopes(auth);
return Array.from(CALENDAR_WRITE_SCOPES).some((scope) => scopes.has(scope));
}

/**
Expand Down Expand Up @@ -166,19 +198,50 @@ export async function getCalendarList(accountId: string): Promise<CalendarInfo[]

const calendar = google.calendar({ version: "v3", auth });
const response = await calendar.calendarList.list();
const accountHasWriteScope = await hasCalendarWriteScope(accountId);
const result: CalendarInfo[] = [];
for (const cal of response.data.items || []) {
if (cal.id) {
const accessRole = cal.accessRole || "";
result.push({
id: cal.id,
name: cal.summary || "Calendar",
color: cal.backgroundColor || "#4285f4",
timezone: cal.timeZone || undefined,
primary: cal.primary || cal.id === "primary",
accessRole,
writable: accountHasWriteScope && (accessRole === "owner" || accessRole === "writer"),
});
}
}
return result;
}

export async function insertCalendarEvent(
accountId: string,
params: CalendarEventInsertParams,
calInfo: { name: string; color: string },
): Promise<CalendarEvent> {
const auth = await getOAuth2Client(accountId);
if (!auth) {
throw new Error("Calendar account is not authenticated");
}

const calendar = google.calendar({ version: "v3", auth });
const response = await calendar.events.insert({
calendarId: params.calendarId,
sendUpdates: params.sendUpdates,
conferenceDataVersion: params.conferenceDataVersion,
requestBody: params.requestBody,
});

const event = parseCalendarEvent(response.data, calInfo);
if (!event) {
throw new Error("Google Calendar did not return a created event");
}
return event;
}

/**
* Sync calendar events using Google's sync token mechanism.
*
Expand Down
Loading