diff --git a/docs/docs/cmd/outlook/calendar/calendar-list.mdx b/docs/docs/cmd/outlook/calendar/calendar-list.mdx
new file mode 100644
index 00000000000..2329aef6cbd
--- /dev/null
+++ b/docs/docs/cmd/outlook/calendar/calendar-list.mdx
@@ -0,0 +1,155 @@
+import Global from '../../_global.mdx';
+import Tabs from '@theme/Tabs';
+import TabItem from '@theme/TabItem';
+
+# outlook calendar list
+
+Retrieves a list of all calendars of a user or a group.
+
+## Usage
+
+```sh
+m365 outlook calendar list [options]
+```
+
+## Options
+
+```md definition-list
+`--userId [userId]`
+: ID of the user. Specify either `userId` or `userName`, but not both.
+
+`--userName [userName]`
+: UPN of the user. Specify either `userId` or `userName`, but not both.
+
+`--calendarGroupId [calendarGroupId]`
+: ID of the calendar group. Specify either `calendarGroupId` or `calendarGroupName`, but not both.
+
+`--calendarGroupName [calendarGroupName]`
+: Name of the calendar group. Specify either `calendarGroupId` or `calendarGroupName`, but not both.
+```
+
+
+
+## Permissions
+
+
+
+
+ | Resource | Permissions |
+ |-----------------|-----------------|
+ | Microsoft Graph | Calendars.Read |
+
+
+
+
+ | Resource | Permissions |
+ |-----------------|-----------------|
+ | Microsoft Graph | Calendars.Read |
+
+
+
+
+## Examples
+
+List all calendars for the current signed-in user.
+
+```sh
+m365 outlook calendar list --userId "@meId"
+```
+
+List all calendars in a specific calendar group for the current signed-in user.
+
+```sh
+m365 outlook calendar list --userId "@meId" --calendarGroupName "Colleague calendars"
+```
+
+List all calendars for a specific user.
+
+```sh
+m365 outlook calendar list --userName "john.doe@contoso.com"
+```
+
+List all calendars from a specific calendar group for a specific user.
+
+```sh
+m365 outlook calendar list --userId b743445a-112c-4fda-9afd-05943f9c7b36 --calendarGroupId "AAMkADIxYjJiYmIzLTFmNjYtNGNhMy0YOkcEEh3vhfAAAGgdFjAAA="
+```
+
+## Response
+
+
+
+
+ ```json
+ [
+ {
+ "id": "AAMkAGI2MDc2YzA0LWQwNTktNGM5Ni05M2VkLWY3NjFkNTUxOTkyZABGAAAAAABeGJMObKvfQbq5qwfGa7kTBwAopDdmUXY8TaLJk5CCLo4zAAAAAAEGAAAopDdmUXY8TaLJk5CCLo4zAAAAAFS0AAA=",
+ "name": "Calendar",
+ "color": "auto",
+ "hexColor": "",
+ "groupClassId": "0006f0b7-0000-0000-c000-000000000046",
+ "isDefaultCalendar": true,
+ "changeKey": "KKQ3ZlF2PE2iyZOQgi6OMwAAAAADcg==",
+ "canShare": true,
+ "canViewPrivateItems": true,
+ "canEdit": true,
+ "allowedOnlineMeetingProviders": [
+ "teamsForBusiness"
+ ],
+ "defaultOnlineMeetingProvider": "teamsForBusiness",
+ "isTallyingResponses": true,
+ "isRemovable": false,
+ "owner": {
+ "name": "John Doe",
+ "address": "john.doe@contoso.com"
+ }
+ }
+ ]
+ ```
+
+
+
+
+ ```text
+ id name
+ -------------------------------------------------------------------------------------------------------------------------------------------------------- ----------------------
+ AAMkAGI2MDc2YzA0LWQwNTktNGM5Ni05M2VkLWY3NjFkNTUxOTkyZABGAAAAAABeGJMObKvfQbq5qwfGa7kTBwAopDdmUXY8TaLJk5CCLo4zAAAAAAEGAAAopDdmUXY8TaLJk5CCLo4zAAAAAFS0AAA= Calendar
+ ```
+
+
+
+
+ ```csv
+ id,name,color,hexColor,groupClassId,isDefaultCalendar,changeKey,canShare,canViewPrivateItems,canEdit,defaultOnlineMeetingProvider,isTallyingResponses,isRemovable
+ AAMkAGI2MDc2YzA0LWQwNTktNGM5Ni05M2VkLWY3NjFkNTUxOTkyZABGAAAAAABeGJMObKvfQbq5qwfGa7kTBwAopDdmUXY8TaLJk5CCLo4zAAAAAAEGAAAopDdmUXY8TaLJk5CCLo4zAAAAAFS0AAA=,Calendar,auto,,0006f0b7-0000-0000-c000-000000000046,1,KKQ3ZlF2PE2iyZOQgi6OMwAAAAADcg==,1,1,1,teamsForBusiness,1,0
+ ```
+
+
+
+
+ ```md
+ # outlook calendar list --userId "0649d0bd-53dc-4e1d-a357-76f1d92d447b"
+
+ Date: 5/11/2026
+
+ ## Calendar (AAMkAGI2MDc2YzA0LWQwNTktNGM5Ni05M2VkLWY3NjFkNTUxOTkyZABGAAAAAABeGJMObKvfQbq5qwfGa7kTBwAopDdmUXY8TaLJk5CCLo4zAAAAAAEGAAAopDdmUXY8TaLJk5CCLo4zAAAAAFS0AAA=)
+
+ Property | Value
+ ---------|-------
+ id | AAMkAGI2MDc2YzA0LWQwNTktNGM5Ni05M2VkLWY3NjFkNTUxOTkyZABGAAAAAABeGJMObKvfQbq5qwfGa7kTBwAopDdmUXY8TaLJk5CCLo4zAAAAAAEGAAAopDdmUXY8TaLJk5CCLo4zAAAAAFS0AAA=
+ name | Calendar
+ color | auto
+ hexColor |
+ groupClassId | 0006f0b7-0000-0000-c000-000000000046
+ isDefaultCalendar | true
+ changeKey | KKQ3ZlF2PE2iyZOQgi6OMwAAAAADcg==
+ canShare | true
+ canViewPrivateItems | true
+ canEdit | true
+ defaultOnlineMeetingProvider | teamsForBusiness
+ isTallyingResponses | true
+ isRemovable | false
+ ```
+
+
+
diff --git a/docs/src/config/sidebars.ts b/docs/src/config/sidebars.ts
index e6e537e6841..03444fe0969 100644
--- a/docs/src/config/sidebars.ts
+++ b/docs/src/config/sidebars.ts
@@ -1314,6 +1314,11 @@ const sidebars: SidebarsConfig = {
label: 'calendar get',
id: 'cmd/outlook/calendar/calendar-get'
},
+ {
+ type: 'doc',
+ label: 'calendar list',
+ id: 'cmd/outlook/calendar/calendar-list'
+ },
{
type: 'doc',
label: 'calendar remove',
diff --git a/src/m365/outlook/commands.ts b/src/m365/outlook/commands.ts
index f66c4ac689c..c4a8efe9ab6 100644
--- a/src/m365/outlook/commands.ts
+++ b/src/m365/outlook/commands.ts
@@ -3,6 +3,7 @@ const prefix: string = 'outlook';
export default {
CALENDAR_ADD: `${prefix} calendar add`,
CALENDAR_GET: `${prefix} calendar get`,
+ CALENDAR_LIST: `${prefix} calendar list`,
CALENDAR_REMOVE: `${prefix} calendar remove`,
CALENDAR_SET: `${prefix} calendar set`,
CALENDARGROUP_GET: `${prefix} calendargroup get`,
diff --git a/src/m365/outlook/commands/calendar/calendar-list.spec.ts b/src/m365/outlook/commands/calendar/calendar-list.spec.ts
new file mode 100644
index 00000000000..08d1ca52e39
--- /dev/null
+++ b/src/m365/outlook/commands/calendar/calendar-list.spec.ts
@@ -0,0 +1,256 @@
+import assert from 'assert';
+import sinon from 'sinon';
+import auth from '../../../../Auth.js';
+import { cli } from '../../../../cli/cli.js';
+import { CommandInfo } from '../../../../cli/CommandInfo.js';
+import { Logger } from '../../../../cli/Logger.js';
+import { CommandError } from '../../../../Command.js';
+import { telemetry } from '../../../../telemetry.js';
+import { pid } from '../../../../utils/pid.js';
+import { session } from '../../../../utils/session.js';
+import { sinonUtil } from '../../../../utils/sinonUtil.js';
+import commands from '../../commands.js';
+import command, { options } from './calendar-list.js';
+import { calendarGroup } from '../../../../utils/calendarGroup.js';
+import { odata } from '../../../../utils/odata.js';
+
+describe(commands.CALENDAR_LIST, () => {
+ const userId = 'ae0e8388-cd70-427f-9503-c57498ee3337';
+ const userName = 'john.doe@contoso.com';
+ const calendarGroupId = 'AQMkADJmMVAAA=';
+ const calendarGroupName = 'My Calendars';
+ const response = [
+ {
+ "id": "AAMkAGI2MDc2YzA0LWQwNTktNGM5Ni05M2VkLWY3NjFkNTUxOTkyZABGAAAAAABeGJMObKvfQbq5qwfGa7kTBwAopDdmUXY8TaLJk5CCLo4zAAAAAAEGAAAopDdmUXY8TaLJk5CCLo4zAAAAAFS0AAA=",
+ "name": "Calendar",
+ "color": "auto",
+ "hexColor": "",
+ "groupClassId": "0006f0b7-0000-0000-c000-000000000046",
+ "isDefaultCalendar": true,
+ "changeKey": "KKQ3ZlF2PE2iyZOQgi6OMwAAAAADcg==",
+ "canShare": true,
+ "canViewPrivateItems": true,
+ "canEdit": true,
+ "allowedOnlineMeetingProviders": [
+ "teamsForBusiness"
+ ],
+ "defaultOnlineMeetingProvider": "teamsForBusiness",
+ "isTallyingResponses": true,
+ "isRemovable": false,
+ "owner": {
+ "name": "John Doe",
+ "address": "john.doe@contoso.com"
+ }
+ },
+ {
+ "id": "AAMkAGI2MDc2YzA0LWQwNTktNGM5Ni05M2VkLWY3NjFkNTUxOTkyZABGAAAAAABeGJMObKvfQbq5qwfGa7kTBwAopDdmUXY8TaLJk5CCLo4zAAAAAAEGAAAopDdmUXY8TaLJk5CCLo4zAAAAAFS1AAA=",
+ "name": "United States holidays",
+ "color": "auto",
+ "hexColor": "",
+ "groupClassId": "0006f0b7-0000-0000-c000-000000000046",
+ "isDefaultCalendar": false,
+ "changeKey": "KKQ3ZlF2PE2iyZOQgi6OMwAAAAADfA==",
+ "canShare": false,
+ "canViewPrivateItems": true,
+ "canEdit": false,
+ "allowedOnlineMeetingProviders": [],
+ "defaultOnlineMeetingProvider": "unknown",
+ "isTallyingResponses": false,
+ "isRemovable": true,
+ "owner": {
+ "name": "John Doe",
+ "address": "john.doe@contoso.com"
+ }
+ },
+ {
+ "id": "AAMkAGI2MDc2YzA0LWQwNTktNGM5Ni05M2VkLWY3NjFkNTUxOTkyZABGAAAAAABeGJMObKvfQbq5qwfGa7kTBwAopDdmUXY8TaLJk5CCLo4zAAAAAAEGAAAopDdmUXY8TaLJk5CCLo4zAAAAAFS4AAA=",
+ "name": "Birthdays",
+ "color": "auto",
+ "hexColor": "",
+ "groupClassId": "0006f0b7-0000-0000-c000-000000000046",
+ "isDefaultCalendar": false,
+ "changeKey": "KKQ3ZlF2PE2iyZOQgi6OMwAAAAAFKg==",
+ "canShare": false,
+ "canViewPrivateItems": true,
+ "canEdit": false,
+ "allowedOnlineMeetingProviders": [],
+ "defaultOnlineMeetingProvider": "unknown",
+ "isTallyingResponses": false,
+ "isRemovable": true,
+ "owner": {
+ "name": "John Doe",
+ "address": "john.doe@contoso.com"
+ }
+ }
+ ];
+
+ let log: any[];
+ let logger: Logger;
+ let loggerLogSpy: sinon.SinonSpy;
+ let commandInfo: CommandInfo;
+ let commandOptionsSchema: typeof options;
+
+ before(() => {
+ sinon.stub(auth, 'restoreAuth').resolves();
+ sinon.stub(telemetry, 'trackEvent').resolves();
+ sinon.stub(pid, 'getProcessName').returns('');
+ sinon.stub(session, 'getId').returns('');
+ auth.connection.active = true;
+ commandInfo = cli.getCommandInfo(command);
+ commandOptionsSchema = commandInfo.command.getSchemaToParse() as typeof options;
+ });
+
+ beforeEach(() => {
+ log = [];
+ logger = {
+ log: async (msg: string) => {
+ log.push(msg);
+ },
+ logRaw: async (msg: string) => {
+ log.push(msg);
+ },
+ logToStderr: async (msg: string) => {
+ log.push(msg);
+ }
+ };
+ loggerLogSpy = sinon.spy(logger, 'log');
+ });
+
+ afterEach(() => {
+ sinonUtil.restore([
+ odata.getAllItems,
+ calendarGroup.getUserCalendarGroupByName
+ ]);
+ });
+
+ after(() => {
+ sinon.restore();
+ auth.connection.active = false;
+ });
+
+ it('has correct name', () => {
+ assert.strictEqual(command.name, commands.CALENDAR_LIST);
+ });
+
+ it('has a description', () => {
+ assert.notStrictEqual(command.description, null);
+ });
+
+ it('defines correct properties for the default output', () => {
+ assert.deepStrictEqual(command.defaultProperties(), ['id', 'name']);
+ });
+
+ it('fails validation if both userId and userName are specified', () => {
+ const actual = commandOptionsSchema.safeParse({ userId, userName });
+ assert.notStrictEqual(actual.success, true);
+ });
+
+ it('passes validation if only userId is specified', () => {
+ const actual = commandOptionsSchema.safeParse({ userId });
+ assert.strictEqual(actual.success, true);
+ });
+
+ it('passes validation if only userName is specified', () => {
+ const actual = commandOptionsSchema.safeParse({ userName });
+ assert.strictEqual(actual.success, true);
+ });
+
+ it('fails validation if userId is not a valid GUID', () => {
+ const actual = commandOptionsSchema.safeParse({ userId: 'foo' });
+ assert.notStrictEqual(actual.success, true);
+ });
+
+ it('fails validation if userName is not a valid UPN', () => {
+ const actual = commandOptionsSchema.safeParse({ userName: 'foo' });
+ assert.notStrictEqual(actual.success, true);
+ });
+
+ it('fails validation if both calendarGroupId and calendarGroupName is specified', () => {
+ const actual = commandOptionsSchema.safeParse({ userId, calendarGroupId, calendarGroupName });
+ assert.notStrictEqual(actual.success, true);
+ });
+
+ it('passes validation if only calendarGroupId is specified', () => {
+ const actual = commandOptionsSchema.safeParse({ userId, calendarGroupId });
+ assert.strictEqual(actual.success, true);
+ });
+
+ it('passes validation if only calendarGroupName is specified', () => {
+ const actual = commandOptionsSchema.safeParse({ userId, calendarGroupName });
+ assert.strictEqual(actual.success, true);
+ });
+
+ it('retrieves calendars for a user by userId', async () => {
+ sinon.stub(odata, 'getAllItems').callsFake(async (url) => {
+ if (url === `https://graph.microsoft.com/v1.0/users('${userId}')/calendars`) {
+ return response;
+ }
+
+ throw 'Invalid request';
+ });
+
+ await command.action(logger, { options: commandOptionsSchema.parse({ userId, verbose: true }) });
+ assert(loggerLogSpy.calledOnceWithExactly(response));
+ });
+
+ it('retrieves calendars for a user by userName', async () => {
+ sinon.stub(odata, 'getAllItems').callsFake(async (url) => {
+ if (url === `https://graph.microsoft.com/v1.0/users('${userName}')/calendars`) {
+ return response;
+ }
+
+ throw 'Invalid request';
+ });
+
+ await command.action(logger, { options: commandOptionsSchema.parse({ userName, verbose: true }) });
+ assert(loggerLogSpy.calledOnceWithExactly(response));
+ });
+
+ it('retrieves calendars for a user and calendar group by calendarGroupId', async () => {
+ sinon.stub(odata, 'getAllItems').callsFake(async (url) => {
+ if (url === `https://graph.microsoft.com/v1.0/users('${userId}')/calendarGroups/${calendarGroupId}/calendars`) {
+ return response;
+ }
+
+ throw 'Invalid request';
+ });
+
+ await command.action(logger, { options: commandOptionsSchema.parse({ userId, calendarGroupId }) });
+ assert(loggerLogSpy.calledOnceWithExactly(response));
+ });
+
+ it('retrieves calendars for a user and calendar group by calendarGroupName', async () => {
+ sinon.stub(calendarGroup, 'getUserCalendarGroupByName').withArgs(userName, calendarGroupName, 'id').resolves({ id: calendarGroupId });
+ sinon.stub(odata, 'getAllItems').callsFake(async (url) => {
+ if (url === `https://graph.microsoft.com/v1.0/users('${userName}')/calendarGroups/${calendarGroupId}/calendars`) {
+ return response;
+ }
+
+ throw 'Invalid request';
+ });
+
+ await command.action(logger, { options: commandOptionsSchema.parse({ userName, calendarGroupName }) });
+ assert(loggerLogSpy.calledOnceWithExactly(response));
+ });
+
+ it('handles error when calendar was not found', async () => {
+ const invalidUserName = 'invalidUser@contoso.com';
+ sinon.stub(odata, 'getAllItems').callsFake(async (url) => {
+ if (url === `https://graph.microsoft.com/v1.0/users('${invalidUserName}')/calendars`) {
+ throw {
+ error:
+ {
+ code: 'ErrorInvalidUser',
+ message: `The requested user '${invalidUserName}' is invalid.`
+ }
+ };
+ }
+ throw `Invalid request`;
+ });
+
+ await assert.rejects(
+ command.action(logger, { options: { userName: invalidUserName } }),
+ new CommandError(`The requested user '${invalidUserName}' is invalid.`)
+ );
+ });
+});
diff --git a/src/m365/outlook/commands/calendar/calendar-list.ts b/src/m365/outlook/commands/calendar/calendar-list.ts
new file mode 100644
index 00000000000..509634b926b
--- /dev/null
+++ b/src/m365/outlook/commands/calendar/calendar-list.ts
@@ -0,0 +1,83 @@
+import { Calendar } from '@microsoft/microsoft-graph-types';
+import { Logger } from '../../../../cli/Logger.js';
+import GraphCommand from '../../../base/GraphCommand.js';
+import commands from '../../commands.js';
+import { z } from 'zod';
+import { globalOptionsZod } from '../../../../Command.js';
+import { validation } from '../../../../utils/validation.js';
+import { calendarGroup } from '../../../../utils/calendarGroup.js';
+import { odata } from '../../../../utils/odata.js';
+
+export const options = z.strictObject({
+ ...globalOptionsZod.shape,
+ userId: z.string()
+ .refine(userId => validation.isValidGuid(userId), {
+ error: e => `'${e.input}' is not a valid GUID.`
+ }).optional(),
+ userName: z.string()
+ .refine(userName => validation.isValidUserPrincipalName(userName), {
+ error: e => `'${e.input}' is not a valid UPN.`
+ }).optional(),
+ calendarGroupId: z.string().optional(),
+ calendarGroupName: z.string().optional()
+});
+
+declare type Options = z.infer;
+
+interface CommandArgs {
+ options: Options;
+}
+
+class OutlookCalendarListCommand extends GraphCommand {
+ public get name(): string {
+ return commands.CALENDAR_LIST;
+ }
+
+ public get description(): string {
+ return 'Retrieves a list of all calendars of a user or a group';
+ }
+
+ public get schema(): z.ZodType | undefined {
+ return options;
+ }
+
+ public getRefinedSchema(schema: typeof options): z.ZodObject | undefined {
+ return schema
+ .refine(options => [options.userId, options.userName].filter(x => x !== undefined).length === 1, {
+ error: 'Specify either userId or userName, but not both'
+ })
+ .refine(options => !(options.calendarGroupId && options.calendarGroupName), {
+ error: 'Specify either calendarGroupId or calendarGroupName, but not both'
+ });
+ }
+
+ public defaultProperties(): string[] | undefined {
+ return ['id', 'name'];
+ }
+
+ public async commandAction(logger: Logger, args: CommandArgs): Promise {
+ if (this.verbose) {
+ await logger.logToStderr(`Retrieving calendars for ${args.options.userId ?? args.options.userName}...`);
+ }
+
+ try {
+ const userIdentifier = args.options.userId ?? args.options.userName;
+ let calendarGroupId = args.options.calendarGroupId;
+
+ if (args.options.calendarGroupName) {
+ const group = await calendarGroup.getUserCalendarGroupByName(userIdentifier!, args.options.calendarGroupName, 'id');
+ calendarGroupId = group.id;
+ }
+
+ const url = `${this.resource}/v1.0/users('${userIdentifier}')/${calendarGroupId ? `calendarGroups/${calendarGroupId}/` : ''}calendars`;
+ const calendars = await odata.getAllItems(url);
+
+ await logger.log(calendars);
+ }
+ catch (err: any) {
+ this.handleRejectedODataJsonPromise(err);
+ }
+ }
+}
+
+export default new OutlookCalendarListCommand();
\ No newline at end of file