diff --git a/docs/docs/cmd/outlook/mailbox/mailbox-settings-get.mdx b/docs/docs/cmd/outlook/mailbox/mailbox-settings-get.mdx new file mode 100644 index 0000000000..537594586f --- /dev/null +++ b/docs/docs/cmd/outlook/mailbox/mailbox-settings-get.mdx @@ -0,0 +1,131 @@ +import Global from '/docs/cmd/_global.mdx'; +import Tabs from '@theme/Tabs'; +import TabItem from '@theme/TabItem'; + +# outlook mailbox settings get + +Get the user's mailbox settings + +## Usage + +```sh +m365 outlook mailbox settings get [options] +``` + +## Options + +```md definition-list +`-i, --userId [userId]` +: The ID of the Microsoft Entra user for which you want to get mailbox settings. Specify either `userId` or `userName`, but not both. This option is required when using application permissions. + +`-n, --userName [userName]` +: The UPN of the Microsoft Entra user for which you want to get mailbox settings. Specify either `userId` or `userName`, but not both. This option is required when using application permissions. +``` + + + +## Examples + +Get mailbox settings of the signed-in user + +```sh +m365 outlook mailbox settings get +``` + +Get mailbox settings of a user specified by id + +```sh +m365 outlook mailbox settings get --userId 1caf7dcd-7e83-4c3a-94f7-932a1299c844 +``` + +## Response + + + + + ```json + { + "archiveFolder": "AQMkAGRlM2Y5YTkzLWI2NzAtNDczOS05YWMyLTJhZGY2MGExMGU0MgAuAAADSG3wPE27kUeySjmT5eRT8QEAfJKVL07sbkmIfHqjbDnRgQAAAgEMAAAA", + "timeZone": "Central Europe Standard Time", + "delegateMeetingMessageDeliveryOptions": "sendToDelegateOnly", + "dateFormat": "dd.MM.yyyy", + "timeFormat": "H:mm", + "userPurpose": "user", + "automaticRepliesSetting": { + "status": "disabled", + "externalAudience": "all", + "internalReplyMessage": "On vacation. Will be back.", + "externalReplyMessage": "Vacation", + "scheduledStartDateTime": { + "dateTime": "2025-01-20T08:00:00.0000000", + "timeZone": "UTC" + }, + "scheduledEndDateTime": { + "dateTime": "2025-01-25T18:00:00.0000000", + "timeZone": "UTC" + } + }, + "language": { + "locale": "en-US", + "displayName": "English (United States)" + }, + "workingHours": { + "daysOfWeek": [ + "monday", + "tuesday", + "wednesday", + "thursday", + "friday" + ], + "startTime": "08:00:00.0000000", + "endTime": "16:30:00.0000000", + "timeZone": { + "name": "Central Europe Standard Time" + } + } + } + ``` + + + + + ```text + archiveFolder : AQMkAGRlM2Y5YTkzLWI2NzAtNDczOS05YWMyLTJhZGY2MGExMGU0MgAuAAADSG3wPE27kUeySjmT5eRT8QEAfJKVL07sbkmIfHqjbDnRgQAAAgEMAAAA + automaticRepliesSetting : {"status":"disabled","externalAudience":"all","internalReplyMessage":"","externalReplyMessage":"","scheduledStartDateTime":{"dateTime":"2025-01-20T08:00:00.0000000","timeZone":"UTC"},"scheduledEndDateTime":{"dateTime":"2025-01-25T18:00:00.0000000","timeZone":"UTC"}} + dateFormat : dd.MM.yyyy + delegateMeetingMessageDeliveryOptions: sendToDelegateOnly + language : {"locale":"en-US","displayName":"English (United States)"} + timeFormat : H:mm + timeZone : Central Europe Standard Time + userPurpose : user + workingHours : {"daysOfWeek":["monday","tuesday","wednesday","thursday","friday"],"startTime":"08:00:00.0000000","endTime":"16:30:00.0000000","timeZone":{"name":"Central Europe Standard Time"}} + ``` + + + + + ```csv + archiveFolder,timeZone,delegateMeetingMessageDeliveryOptions,dateFormat,timeFormat,userPurpose + AQMkAGRlM2Y5YTkzLWI2NzAtNDczOS05YWMyLTJhZGY2MGExMGU0MgAuAAADSG3wPE27kUeySjmT5eRT8QEAfJKVL07sbkmIfHqjbDnRgQAAAgEMAAAA,Central Europe Standard Time,sendToDelegateOnly,dd.MM.yyyy,H:mm,user + ``` + + + + + ```md + # outlook mailbox settings get + + Date: 1/17/2025 + + Property | Value + ---------|------- + archiveFolder | AQMkAGRlM2Y5YTkzLWI2NzAtNDczOS05YWMyLTJhZGY2MGExMGU0MgAuAAADSG3wPE27kUeySjmT5eRT8QEAfJKVL07sbkmIfHqjbDnRgQAAAgEMAAAA + timeZone | Central Europe Standard Time + delegateMeetingMessageDeliveryOptions | sendToDelegateOnly + dateFormat | dd.MM.yyyy + timeFormat | H:mm + userPurpose | user + ``` + + + \ No newline at end of file diff --git a/docs/src/config/sidebars.ts b/docs/src/config/sidebars.ts index 8302d3b8d0..560915ae81 100644 --- a/docs/src/config/sidebars.ts +++ b/docs/src/config/sidebars.ts @@ -1184,6 +1184,11 @@ const sidebars: SidebarsConfig = { }, { mailbox: [ + { + type: 'doc', + label: 'mailbox settings get', + id: 'cmd/outlook/mailbox/mailbox-settings-get' + }, { type: 'doc', label: 'mailbox settings set', diff --git a/src/m365/outlook/commands.ts b/src/m365/outlook/commands.ts index a9ae8fc3f2..1600259124 100644 --- a/src/m365/outlook/commands.ts +++ b/src/m365/outlook/commands.ts @@ -2,6 +2,7 @@ const prefix: string = 'outlook'; export default { MAIL_SEND: `${prefix} mail send`, + MAILBOX_SETTINGS_GET: `${prefix} mailbox settings get`, MAILBOX_SETTINGS_SET: `${prefix} mailbox settings set`, MESSAGE_GET: `${prefix} message get`, MESSAGE_LIST: `${prefix} message list`, diff --git a/src/m365/outlook/commands/mailbox/mailbox-settings-get.spec.ts b/src/m365/outlook/commands/mailbox/mailbox-settings-get.spec.ts new file mode 100644 index 0000000000..26ab90c6d5 --- /dev/null +++ b/src/m365/outlook/commands/mailbox/mailbox-settings-get.spec.ts @@ -0,0 +1,253 @@ +import assert from 'assert'; +import sinon from 'sinon'; +import auth from '../../../../Auth.js'; +import commands from '../../commands.js'; +import request from '../../../../request.js'; +import { Logger } from '../../../../cli/Logger.js'; +import { telemetry } from '../../../../telemetry.js'; +import { pid } from '../../../../utils/pid.js'; +import { session } from '../../../../utils/session.js'; +import command from './mailbox-settings-get.js'; +import { sinonUtil } from '../../../../utils/sinonUtil.js'; +import { CommandError } from '../../../../Command.js'; +import { z } from 'zod'; +import { CommandInfo } from '../../../../cli/CommandInfo.js'; +import { cli } from '../../../../cli/cli.js'; +import { accessToken } from '../../../../utils/accessToken.js'; + +describe(commands.MAILBOX_SETTINGS_GET, () => { + const userId = 'abcd1234-de71-4623-b4af-96380a352509'; + const userName = 'john.doe@contoso.com'; + + const mailboxSettingsResponse = { + "timeZone": "Central Europe Standard Time", + "delegateMeetingMessageDeliveryOptions": "sendToDelegateAndInformationToPrincipal", + "dateFormat": "dd.MM.yyyy", + "timeFormat": "HH:mm", + "userPurpose": "user", + "automaticRepliesSetting": { + "status": "disabled", + "externalAudience": "none", + "internalReplyMessage": "I'm out of office. Contact my manager in case of any troubles.", + "externalReplyMessage": "I'm out of office", + "scheduledStartDateTime": { + "dateTime": "2025-01-03T19:00:00.0000000", + "timeZone": "UTC" + }, + "scheduledEndDateTime": { + "dateTime": "2025-01-04T19:00:00.0000000", + "timeZone": "UTC" + } + }, + "language": { + "locale": "cs-CZ", + "displayName": "Czech (Czech Republic)" + }, + "workingHours": { + "daysOfWeek": [ + "monday", + "tuesday", + "wednesday", + "thursday", + "friday" + ], + "startTime": "07:00:00.0000000", + "endTime": "16:00:00.0000000", + "timeZone": { + "name": "Central Europe Standard Time" + } + } + }; + + let log: string[]; + let logger: Logger; + let commandInfo: CommandInfo; + let loggerLogSpy: sinon.SinonSpy; + let commandOptionsSchema: z.ZodTypeAny; + + before(() => { + sinon.stub(auth, 'restoreAuth').resolves(); + sinon.stub(telemetry, 'trackEvent').returns(); + sinon.stub(pid, 'getProcessName').returns(''); + sinon.stub(session, 'getId').returns(''); + auth.connection.active = true; + if (!auth.connection.accessTokens[auth.defaultResource]) { + auth.connection.accessTokens[auth.defaultResource] = { + expiresOn: 'abc', + accessToken: 'abc' + }; + } + commandInfo = cli.getCommandInfo(command); + commandOptionsSchema = commandInfo.command.getSchemaToParse()!; + }); + + 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'); + sinon.stub(accessToken, 'isAppOnlyAccessToken').returns(false); + }); + + afterEach(() => { + sinonUtil.restore([ + accessToken.isAppOnlyAccessToken, + request.get + ]); + }); + + after(() => { + sinon.restore(); + auth.connection.active = false; + }); + + it('has correct name', () => { + assert.strictEqual(command.name, commands.MAILBOX_SETTINGS_GET); + }); + + it('has a description', () => { + assert.notStrictEqual(command.description, null); + }); + + it('fails validation if both userId and userName are specified', () => { + sinonUtil.restore(accessToken.isAppOnlyAccessToken); + sinon.stub(accessToken, 'isAppOnlyAccessToken').returns(true); + + const actual = commandOptionsSchema.safeParse({ + userId: userId, + userName: userName + }); + assert.notStrictEqual(actual.success, true); + }); + + it('fails validation if userId is not a valid GUID', () => { + sinonUtil.restore(accessToken.isAppOnlyAccessToken); + sinon.stub(accessToken, 'isAppOnlyAccessToken').returns(true); + + const actual = commandOptionsSchema.safeParse({ + userId: 'foo' + }); + assert.notStrictEqual(actual.success, true); + }); + + it('fails validation if userName is not a valid UPN', () => { + sinonUtil.restore(accessToken.isAppOnlyAccessToken); + sinon.stub(accessToken, 'isAppOnlyAccessToken').returns(true); + + const actual = commandOptionsSchema.safeParse({ + userName: 'foo' + }); + assert.notStrictEqual(actual.success, true); + }); + + it('retrieves mailbox settings of the signed-in user', async () => { + sinonUtil.restore(accessToken.isAppOnlyAccessToken); + sinon.stub(accessToken, 'isAppOnlyAccessToken').returns(false); + + sinon.stub(request, 'get').callsFake(async (opts) => { + if (opts.url === 'https://graph.microsoft.com/v1.0/me/mailboxSettings') { + return mailboxSettingsResponse; + } + + throw 'Invalid request'; + }); + + const result = commandOptionsSchema.safeParse({ + verbose: true + }); + + await command.action(logger, { + options: result.data + }); + assert(loggerLogSpy.calledOnceWith(mailboxSettingsResponse)); + }); + + it('retrieves mailbox settings of a user specified by id', async () => { + sinonUtil.restore(accessToken.isAppOnlyAccessToken); + sinon.stub(accessToken, 'isAppOnlyAccessToken').returns(true); + + sinon.stub(request, 'get').callsFake(async (opts) => { + if (opts.url === `https://graph.microsoft.com/v1.0/users('${ userId }')/mailboxSettings`) { + return mailboxSettingsResponse; + } + + throw 'Invalid request'; + }); + + const result = commandOptionsSchema.safeParse({ + userId: userId, + verbose: true + }); + + await command.action(logger, { + options: result.data + }); + assert(loggerLogSpy.calledOnceWith(mailboxSettingsResponse)); + }); + + it('retrieves mailbox settings of a user specified by user principal name', async () => { + sinonUtil.restore(accessToken.isAppOnlyAccessToken); + sinon.stub(accessToken, 'isAppOnlyAccessToken').returns(true); + + sinon.stub(request, 'get').callsFake(async (opts) => { + if (opts.url === `https://graph.microsoft.com/v1.0/users('${userName}')/mailboxSettings`) { + return mailboxSettingsResponse; + } + + throw 'Invalid request'; + }); + + const result = commandOptionsSchema.safeParse({ + userName: userName, + verbose: true + }); + + await command.action(logger, { + options: result.data + }); + assert(loggerLogSpy.calledOnceWith(mailboxSettingsResponse)); + }); + + it('fails retrieve mailbox settings if neither userId nor userName is specified in app-only mode', async () => { + sinonUtil.restore(accessToken.isAppOnlyAccessToken); + sinon.stub(accessToken, 'isAppOnlyAccessToken').returns(true); + + const result = commandOptionsSchema.safeParse({ verbose: true }); + + await assert.rejects(command.action(logger, { options: result.data }), new CommandError('When running with application permissions either userId or userName is required')); + }); + + it('fails retrieve mailbox settings of the signed-in user if userId is specified', async () => { + const result = commandOptionsSchema.safeParse({ userId: userId, verbose: true }); + await assert.rejects(command.action(logger, { options: result.data }), new CommandError('You can retrieve mailbox settings of other users only if CLI is authenticated in app-only mode')); + }); + + it('fails retrieve mailbox settings of the signed-in user if userName is specified', async () => { + const result = commandOptionsSchema.safeParse({ userName: userName, verbose: true }); + await assert.rejects(command.action(logger, { options: result.data }), new CommandError('You can retrieve mailbox settings of other users only if CLI is authenticated in app-only mode')); + }); + + it('correctly handles API OData error', async () => { + sinon.stub(request, 'get').rejects({ + error: { + 'odata.error': { + code: '-1, InvalidOperationException', + message: { + value: 'Invalid request' + } + } + } + }); + const result = commandOptionsSchema.safeParse({ verbose: true }); + await assert.rejects(command.action(logger, { options: result.data }), new CommandError('Invalid request')); + }); +}); \ No newline at end of file diff --git a/src/m365/outlook/commands/mailbox/mailbox-settings-get.ts b/src/m365/outlook/commands/mailbox/mailbox-settings-get.ts new file mode 100644 index 0000000000..03e119ea59 --- /dev/null +++ b/src/m365/outlook/commands/mailbox/mailbox-settings-get.ts @@ -0,0 +1,92 @@ +import { z } from 'zod'; +import { globalOptionsZod } from '../../../../Command.js'; +import { zod } from '../../../../utils/zod.js'; +import GraphCommand from '../../../base/GraphCommand.js'; +import { Logger } from '../../../../cli/Logger.js'; +import commands from '../../commands.js'; +import { validation } from '../../../../utils/validation.js'; +import request, { CliRequestOptions } from '../../../../request.js'; +import { MailboxSettings } from '@microsoft/microsoft-graph-types'; +import { accessToken } from '../../../../utils/accessToken.js'; +import auth from '../../../../Auth.js'; + +const options = globalOptionsZod + .extend({ + userId: zod.alias('i', z.string().refine(id => validation.isValidGuid(id), id => ({ + message: `'${id}' is not a valid GUID.` + })).optional()), + userName: zod.alias('n', z.string().refine(name => validation.isValidUserPrincipalName(name), name => ({ + message: `'${name}' is not a valid UPN.` + })).optional()) + }) + .strict(); + +declare type Options = z.infer; + +interface CommandArgs { + options: Options; +} + +class OutlookMailboxSettingsGetCommand extends GraphCommand { + public get name(): string { + return commands.MAILBOX_SETTINGS_GET; + } + + public get description(): string { + return `Get the user's mailbox settings`; + } + + public get schema(): z.ZodTypeAny | undefined { + return options; + } + + public getRefinedSchema(schema: typeof options): z.ZodEffects | undefined { + return schema + .refine(options => !(options.userId && options.userName), { + message: 'Specify either userId or userName, but not both' + }); + } + + public async commandAction(logger: Logger, args: CommandArgs): Promise { + const isAppOnlyAccessToken = accessToken.isAppOnlyAccessToken(auth.connection.accessTokens[auth.defaultResource].accessToken); + + let requestUrl = `${this.resource}/v1.0/me/mailboxSettings`; + + if (isAppOnlyAccessToken) { + if (!args.options.userId && !args.options.userName) { + throw 'When running with application permissions either userId or userName is required'; + } + + const userIdentifier = args.options.userId ?? args.options.userName; + + if (this.verbose) { + await logger.logToStderr(`Retrieving mailbox settings for user ${userIdentifier}...`); + } + + requestUrl = `${this.resource}/v1.0/users('${userIdentifier}')/mailboxSettings`; + } + else { + if (args.options.userId || args.options.userName) { + throw 'You can retrieve mailbox settings of other users only if CLI is authenticated in app-only mode'; + } + } + + const requestOptions: CliRequestOptions = { + url: requestUrl, + headers: { + accept: 'application/json;odata.metadata=none' + }, + responseType: 'json' + }; + + try { + const result = await request.get(requestOptions); + await logger.log(result); + } + catch (err: any) { + this.handleRejectedODataJsonPromise(err); + } + } +} + +export default new OutlookMailboxSettingsGetCommand(); \ No newline at end of file diff --git a/src/m365/outlook/commands/mailbox/mailbox-settings-set.spec.ts b/src/m365/outlook/commands/mailbox/mailbox-settings-set.spec.ts index 3c08d6eeff..1901cbce82 100644 --- a/src/m365/outlook/commands/mailbox/mailbox-settings-set.spec.ts +++ b/src/m365/outlook/commands/mailbox/mailbox-settings-set.spec.ts @@ -118,7 +118,7 @@ describe(commands.MAILBOX_SETTINGS_SET, () => { assert.notStrictEqual(command.description, null); }); - it('fails validation if both userId and userName are provided in app-only mode', () => { + it('fails validation if both userId and userName are specified', () => { sinonUtil.restore(accessToken.isAppOnlyAccessToken); sinon.stub(accessToken, 'isAppOnlyAccessToken').returns(true); @@ -599,13 +599,13 @@ describe(commands.MAILBOX_SETTINGS_SET, () => { }); }); - it('fails updating mailbox settings if both userId and userName is specified in app-only mode', async () => { + it('fails updating mailbox settings if neither userId nor userName is specified in app-only mode', async () => { sinonUtil.restore(accessToken.isAppOnlyAccessToken); sinon.stub(accessToken, 'isAppOnlyAccessToken').returns(true); - const result = commandOptionsSchema.safeParse({ userId: userId, userName: userName, timeFormat: 'HH:mm', verbose: true }); + const result = commandOptionsSchema.safeParse({ timeFormat: 'HH:mm', verbose: true }); - await assert.rejects(command.action(logger, { options: result.data }), new CommandError('When running with application permissions either userId or userName is required, but not both')); + await assert.rejects(command.action(logger, { options: result.data }), new CommandError('When running with application permissions either userId or userName is required')); }); it('fails updating mailbox settings of the signed-in user if userId is specified', async () => { diff --git a/src/m365/outlook/commands/mailbox/mailbox-settings-set.ts b/src/m365/outlook/commands/mailbox/mailbox-settings-set.ts index 948633d370..8c941d4ef2 100644 --- a/src/m365/outlook/commands/mailbox/mailbox-settings-set.ts +++ b/src/m365/outlook/commands/mailbox/mailbox-settings-set.ts @@ -59,6 +59,9 @@ class OutlookMailboxSettingsSetCommand extends GraphCommand { public getRefinedSchema(schema: typeof options): z.ZodEffects | undefined { return schema + .refine(options => !(options.userId && options.userName), { + message: 'Specify either userId or userName, but not both' + }) .refine(options => [options.workingDays, options.workingHoursStartTime, options.workingHoursEndTime, options.workingHoursTimeZone, options.autoReplyStatus, options.autoReplyExternalAudience, options.autoReplyExternalMessage, options.autoReplyInternalMessage, options.autoReplyStartDateTime, options.autoReplyStartTimeZone, options.autoReplyEndDateTime, options.autoReplyEndTimeZone, @@ -73,8 +76,8 @@ class OutlookMailboxSettingsSetCommand extends GraphCommand { let requestUrl = `${this.resource}/v1.0/me/mailboxSettings`; if (isAppOnlyAccessToken) { - if (args.options.userId && args.options.userName) { - throw 'When running with application permissions either userId or userName is required, but not both'; + if (!args.options.userId && !args.options.userName) { + throw 'When running with application permissions either userId or userName is required'; } const userIdentifier = args.options.userId ?? args.options.userName;