From 4866e7b498a686358fe17f105deb417d0f4f0300 Mon Sep 17 00:00:00 2001 From: Martin Machacek Date: Mon, 6 Jan 2025 12:10:25 +0100 Subject: [PATCH 1/6] New command: outlook mailbox settings set --- .eslintrc.cjs | 1 + .../outlook/mailbox/mailbox-settings-set.mdx | 166 +++++ docs/src/config/sidebars.ts | 9 + src/m365/outlook/commands.ts | 1 + .../mailbox/mailbox-settings-set.spec.ts | 606 ++++++++++++++++++ .../commands/mailbox/mailbox-settings-set.ts | 204 ++++++ 6 files changed, 987 insertions(+) create mode 100644 docs/docs/cmd/outlook/mailbox/mailbox-settings-set.mdx create mode 100644 src/m365/outlook/commands/mailbox/mailbox-settings-set.spec.ts create mode 100644 src/m365/outlook/commands/mailbox/mailbox-settings-set.ts diff --git a/.eslintrc.cjs b/.eslintrc.cjs index 253f96a74b8..87d6eaa2fb5 100644 --- a/.eslintrc.cjs +++ b/.eslintrc.cjs @@ -67,6 +67,7 @@ const dictionary = [ 'log', 'login', 'logout', + 'mailbox', 'management', 'member', 'membership', diff --git a/docs/docs/cmd/outlook/mailbox/mailbox-settings-set.mdx b/docs/docs/cmd/outlook/mailbox/mailbox-settings-set.mdx new file mode 100644 index 00000000000..47069811fbc --- /dev/null +++ b/docs/docs/cmd/outlook/mailbox/mailbox-settings-set.mdx @@ -0,0 +1,166 @@ +import Global from '/docs/cmd/_global.mdx'; +import Tabs from '@theme/Tabs'; +import TabItem from '@theme/TabItem'; + +# outlook mailbox settings set + +Updates user mailbox settings + +## Usage + +```sh +m365 outlook mailbox settings set [options] +``` + +## Options + +```md definition-list +`-i, --userId [userId]` +: The ID of the Microsoft Entra user to update mailbox settings for. 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 to update mailbox settings for. Specify either `userId` or `userName`, but not both. This option is required when using application permissions. + +`--dateFormat [dateFormat]` +: The date format for the user's mailbox. Example: `dd.MM.yyyy`. + +`--timeFormat [timeFormat]` +: The time format for the user's mailbox. Example: `H:mm`. + +`--timeZone [timeZone]` +: The default time zone for the user's mailbox. Should follow [Windows time zone name](https://learn.microsoft.com/en-us/windows-hardware/manufacture/desktop/default-time-zones?view=windows-11#time-zones) or [IANA time zone identifier](https://en.wikipedia.org/wiki/List_of_tz_database_time_zones#List). Example: `Central Europe Standard Time`. + +`--language [language]` +: The preferred language for the user. Should follow [ISO 639-1 Code](https://learn.microsoft.com/openspecs/office_standards/ms-oe376/6c085406-a698-4e12-9d4d-c3b0ee3dbc4a). Example: `en-US`. + +`--delegateMeetingMessageDeliveryOptions [delegateMeetingMessageDeliveryOptions]` +: Specifies who can receive meeting messages and meeting responses. Allowed values are `sendToDelegateOnly`, `sendToDelegateAndPrincipal`, or `sendToDelegateAndInformationToPrincipal`. + +`--workingDays [workingDays]` +: The days of the week on which the user works, separated by a comma. Allowed values are `monday`, `tuesday`, `wednesday`, `thursday`, `friday`, `saturday`, or `sunday`. + +`--workingHoursStartTime [workingHoursStartTime]` +: The time of the day that the user starts working. Example: `09:00:00.000000`. + +`--workingHoursEndTime [workingHoursEndTime]` +: The time of the day that the user stops working. Example: `17:00:00.000000`. + +`--workingHoursTimeZone [workingHoursTimeZone]` +: The name of a time zone to which the working hours apply. Should follow [Windows time zone name](https://learn.microsoft.com/en-us/windows-hardware/manufacture/desktop/default-time-zones?view=windows-11#time-zones) or [IANA time zone identifier](https://en.wikipedia.org/wiki/List_of_tz_database_time_zones#List). Example: `Central Europe Standard Time`. + +`--autoReplyExternalAudience [autoReplyExternalAudience]` +: Specifies external audience who will receive reply message. Allowed values are `none`, `contactsOnly`, or `all`. + +`--autoReplyExternalMessage [autoReplyExternalMessage]` +: The reply message for the external audience. + +`--autoReplyInternalMessage [autoReplyInternalMessage]` +: The reply message for the audience from the signed-in user's organization. + +`--autoReplyStartDateTime [autoReplyStartDateTime]` +: The date and time that automatic replies are set to begin. Example: `2025-01-06T11:00:00.0000000`. + +`--autoReplyStartTimeZone [autoReplyStartTimeZone]` +: The time zone that automatic replies are set to begin. + +`--autoReplyEndDateTime [autoReplyEndDateTime]` +: The date and time that automatic replies are set to end. Example: `2025-01-07T11:00:00.0000000`. + +`--autoReplyEndTimeZone [autoReplyEndTimeZone]` +: The time zone that automatic replies are set to end. + +`--autoReplyStatus [autoReplyStatus]` +: The status for automatic replies. Allowed values are `disabled`, `alwaysEnabled`, or `scheduled`. +``` + + + +## Examples + +Update date, time format and time zone of the signed-in user. + +```sh +m365 outlook mailbox settings set --dateFormat 'dd.MM.yyyy' --timeFormat 'H:mm' --timeZone 'Central Europe Standard Time' --language 'en-US' +``` + +Update working hours of a user specified by id + +```sh +m365 outlook mailbox settings set --userId 1caf7dcd-7e83-4c3a-94f7-932a1299c844 --workingDays 'monday,tuesday,thursday,friday' --workingHoursStartTime '08:00:00.0000000' --workingHoursEndTime '16:30:00.0000000' --workingHoursTimeZone 'Central Europe Standard Time' +``` + +Set scheduled automatic replies for the internal audience of a user specified by UPN + +```sh +m365 outlook mailbox settings set --userName john.doe@contoso.com --autoReplyExternalAudience none --autoReplyInternalMessage 'On vacation' --autoReplyStartDateTime '2024-08-05T08:00:00.0000000' --autoReplyStartTimeZone 'Central Europe Standard Time' --autoReplyEndDateTime '2024-08-09T16:00:00.0000000' --autoReplyEndTimeZone 'Central Europe Standard Time' --autoReplyStatus scheduled +``` + +## Response + +The command returns the updated properties of the user mailbox settings. + + + + + ```json + { + "timeZone": "Central Europe Standard Time", + "timeFormat": "H:mm", + "dateFormat": "dd.MM.yyyy", + "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 + dateFormat : dd.MM.yyyy + language : {"locale":"en-US","displayName":"English (United States)"} + timeFormat : H:mm + timeZone : Central Europe Standard Time + workingHours: {"daysOfWeek":["monday","tuesday","wednesday","thursday","friday"],"startTime":"08:00:00.0000000","endTime":"16:30:00.0000000","timeZone":{"name":"Central Europe Standard Time"}} + ``` + + + + + ```csv + dateFormat,timeZone,timeFormat + dd.MM.yyy,Central Europe Standard Time,H:mm + ``` + + + + + ```md + # outlook mailbox settings set --dateFormat "dd.MM.yyyy" --timeFormat "H:mm" --timeZone "Central Europe Standard Time" --language "en-US" --workingDays "monday,tuesday,wednesday,thursday,friday" --workingHoursStartTime "08:00:00.0000000" --workingHoursEndTime "16:30:00.0000000" --workingHoursTimeZone "Central Europe Standard Time" + + Date: 1/6/2025 + + Property | Value + ---------|------- + dateFormat | dd.MM.yyyy + timeZone | Central Europe Standard Time + timeFormat | H:mm + ``` + + + \ No newline at end of file diff --git a/docs/src/config/sidebars.ts b/docs/src/config/sidebars.ts index 68fa5ec4249..9f9fdd2640d 100644 --- a/docs/src/config/sidebars.ts +++ b/docs/src/config/sidebars.ts @@ -1149,6 +1149,15 @@ const sidebars: SidebarsConfig = { } ] }, + { + mailbox: [ + { + type: 'doc', + label: 'mailbox settings set', + id: 'cmd/outlook/mailbox/mailbox-settings-set' + } + ] + }, { message: [ { diff --git a/src/m365/outlook/commands.ts b/src/m365/outlook/commands.ts index ace9d6c5935..a9ae8fc3f23 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_SET: `${prefix} mailbox settings set`, MESSAGE_GET: `${prefix} message get`, MESSAGE_LIST: `${prefix} message list`, MESSAGE_MOVE: `${prefix} message move`, diff --git a/src/m365/outlook/commands/mailbox/mailbox-settings-set.spec.ts b/src/m365/outlook/commands/mailbox/mailbox-settings-set.spec.ts new file mode 100644 index 00000000000..d5073c1d68d --- /dev/null +++ b/src/m365/outlook/commands/mailbox/mailbox-settings-set.spec.ts @@ -0,0 +1,606 @@ +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-set.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_SET, () => { + 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'); + //(command as any).pollingInterval = 0; + sinon.stub(accessToken, 'isAppOnlyAccessToken').returns(false); + }); + + afterEach(() => { + sinonUtil.restore([ + accessToken.isAppOnlyAccessToken, + request.patch + ]); + }); + + after(() => { + sinon.restore(); + auth.connection.active = false; + }); + + it('has correct name', () => { + assert.strictEqual(command.name, commands.MAILBOX_SETTINGS_SET); + }); + + it('has a description', () => { + assert.notStrictEqual(command.description, null); + }); + + it('fails validation if both userId and userName are provided in app-only mode', () => { + 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 provided in delegated mode', () => { + const actual = commandOptionsSchema.safeParse({ userId: userId }); + assert.notStrictEqual(actual.success, true); + }); + + it('fails validation if userName is provided in delegated mode', () => { + const actual = commandOptionsSchema.safeParse({ userName: userName }); + assert.notStrictEqual(actual.success, true); + }); + + it('fails validation if both userId and userName are provided in delegated mode', () => { + 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('fails validation if no option except user id provided in app-only mode', () => { + sinonUtil.restore(accessToken.isAppOnlyAccessToken); + sinon.stub(accessToken, 'isAppOnlyAccessToken').returns(true); + + const actual = commandOptionsSchema.safeParse({ + userId: userId + }); + assert.notStrictEqual(actual.success, true); + }); + + it('fails validation if no option is provided in delegated mode', () => { + const actual = commandOptionsSchema.safeParse({}); + assert.notStrictEqual(actual.success, true); + }); + + it('fails validation if delegateMeetingMessageDeliveryOptions has wrong value', () => { + const actual = commandOptionsSchema.safeParse({ + delegateMeetingMessageDeliveryOptions: 'foo' + }); + assert.notStrictEqual(actual.success, true); + }); + + it('fails validation if workingDays has wrong value', () => { + const actual = commandOptionsSchema.safeParse({ + workingDays: 'foo' + }); + assert.notStrictEqual(actual.success, true); + }); + + it('fails validation if autoReplyExternalAudience has wrong value', () => { + const actual = commandOptionsSchema.safeParse({ + autoReplyExternalAudience: 'foo' + }); + assert.notStrictEqual(actual.success, true); + }); + + it('fails validation if autoReplyStatus has wrong value', () => { + const actual = commandOptionsSchema.safeParse({ + autoReplyStatus: 'foo' + }); + assert.notStrictEqual(actual.success, true); + }); + + it('updates mailbox settings of the signed-in user', async () => { + sinonUtil.restore(accessToken.isAppOnlyAccessToken); + sinon.stub(accessToken, 'isAppOnlyAccessToken').returns(false); + + sinon.stub(request, 'patch').callsFake(async (opts) => { + if (opts.url === 'https://graph.microsoft.com/v1.0/me/mailboxSettings') { + return mailboxSettingsResponse; + } + + throw 'Invalid request'; + }); + + await command.action(logger, { + options: { + dateFormat: 'dd.MM.yyy', + timeFormat: 'HH:mm', + timeZone: 'Central Europe Standard Time', + language: 'en-US', + delegateMeetingMessageDeliveryOptions: 'sendToDelegateAndInformationToPrincipal', + workingDays: 'monday,tuesday,wednesday,thursday,friday', + workingHoursStartTime: '09:00:00.000000', + workingHoursEndTime: '17:00:00.000000', + workingHoursTimeZone: 'UTC', + autoReplyExternalAudience: 'contactsOnly', + autoReplyExternalMessage: "I'm out of office", + autoReplyInternalMessage: "I'm out of office. Contact my manager in case of any troubles.", + autoReplyStartDateTime: '2025-01-06T00:00:00.0000000', + autoReplyStartTimeZone: 'UTC', + autoReplyEndDateTime: '2025-01-10T00:00:00.0000000', + autoReplyEndTimeZone: 'UTC', + autoReplyStatus: 'scheduled', + verbose: true + } + }); + assert(loggerLogSpy.calledOnceWith(mailboxSettingsResponse)); + }); + + it('updates working hours of a user specified by id', async () => { + sinonUtil.restore(accessToken.isAppOnlyAccessToken); + sinon.stub(accessToken, 'isAppOnlyAccessToken').returns(true); + + const patchStub = sinon.stub(request, 'patch').callsFake(async (opts) => { + if (opts.url === `https://graph.microsoft.com/v1.0/users('${userId}')/mailboxSettings`) { + return mailboxSettingsResponse; + } + + throw 'Invalid request'; + }); + + await command.action(logger, { + options: { + userId: userId, + workingDays: "monday,tuesday,wednesday,thursday,friday", + workingHoursStartTime: '09:00:00.000000', + workingHoursEndTime: '17:00:00.000000', + workingHoursTimeZone: 'UTC', + verbose: true + } + }); + + assert.deepStrictEqual(patchStub.lastCall.args[0].data, { + workingHours: { + daysOfWeek: [ + "monday", + "tuesday", + "wednesday", + "thursday", + "friday" + ], + startTime: "09:00:00.000000", + endTime: "17:00:00.000000", + timeZone: { + name: "UTC" + } + } + }); + }); + + it('updates working days of signed-in user', async () => { + const patchStub = sinon.stub(request, 'patch').callsFake(async (opts) => { + if (opts.url === `https://graph.microsoft.com/v1.0/me/mailboxSettings`) { + throw opts.data; + } + + throw 'Invalid request'; + }); + + await command.action(logger, { + options: { + workingDays: 'monday,tuesday,wednesday,thursday,friday' + } + }); + + assert.deepStrictEqual(patchStub.lastCall.args[0].data, { + workingHours: { + daysOfWeek: [ + "monday", + "tuesday", + "wednesday", + "thursday", + "friday" + ] + } + }); + }); + + it('updates working hours timezone of a user specified by UPN', async () => { + sinonUtil.restore(accessToken.isAppOnlyAccessToken); + sinon.stub(accessToken, 'isAppOnlyAccessToken').returns(true); + + const patchStub = sinon.stub(request, 'patch').callsFake(async (opts) => { + if (opts.url === `https://graph.microsoft.com/v1.0/users('${userName}')/mailboxSettings`) { + return mailboxSettingsResponse; + } + + throw 'Invalid request'; + }); + + await command.action(logger, { + options: { + userName: userName, + workingHoursTimeZone: 'UTC', + verbose: true + } + }); + + assert.deepStrictEqual(patchStub.lastCall.args[0].data, { + workingHours: { + timeZone: { + name: "UTC" + } + } + }); + }); + + it('updates working hours start time of the signed-in user', async () => { + const patchStub = sinon.stub(request, 'patch').callsFake(async (opts) => { + if (opts.url === 'https://graph.microsoft.com/v1.0/me/mailboxSettings') { + return mailboxSettingsResponse; + } + + throw 'Invalid request'; + }); + + await command.action(logger, { + options: { + workingHoursStartTime: '07:00:00.0000000' + } + }); + + assert.deepStrictEqual(patchStub.lastCall.args[0].data, { + workingHours: { + startTime: '07:00:00.0000000' + } + }); + }); + + it('updates working hours end time of the signed-in user', async () => { + const patchStub = sinon.stub(request, 'patch').callsFake(async (opts) => { + if (opts.url === 'https://graph.microsoft.com/v1.0/me/mailboxSettings') { + return mailboxSettingsResponse; + } + + throw 'Invalid request'; + }); + + await command.action(logger, { + options: { + workingHoursEndTime: '16:00:00.0000000' + } + }); + + assert.deepStrictEqual(patchStub.lastCall.args[0].data, { + workingHours: { + endTime: '16:00:00.0000000' + } + }); + }); + + it('updates auto reply external audience of the signed-in user', async () => { + const patchStub = sinon.stub(request, 'patch').callsFake(async (opts) => { + if (opts.url === 'https://graph.microsoft.com/v1.0/me/mailboxSettings') { + return mailboxSettingsResponse; + } + + throw 'Invalid request'; + }); + + await command.action(logger, { + options: { + autoReplyExternalAudience: 'all' + } + }); + + assert.deepStrictEqual(patchStub.lastCall.args[0].data, { + automaticRepliesSetting: { + externalAudience: 'all' + } + }); + }); + + it('updates auto reply external message of the signed-in user', async () => { + const patchStub = sinon.stub(request, 'patch').callsFake(async (opts) => { + if (opts.url === 'https://graph.microsoft.com/v1.0/me/mailboxSettings') { + return mailboxSettingsResponse; + } + + throw 'Invalid request'; + }); + + await command.action(logger, { + options: { + autoReplyExternalMessage: `I'm out of office` + } + }); + + assert.deepStrictEqual(patchStub.lastCall.args[0].data, { + automaticRepliesSetting: { + externalReplyMessage: `I'm out of office` + } + }); + }); + + it('updates auto reply internal message of the signed-in user', async () => { + const patchStub = sinon.stub(request, 'patch').callsFake(async (opts) => { + if (opts.url === 'https://graph.microsoft.com/v1.0/me/mailboxSettings') { + return mailboxSettingsResponse; + } + + throw 'Invalid request'; + }); + + await command.action(logger, { + options: { + autoReplyInternalMessage: `I'm out of office. Contact my manager in case of any troubles.` + } + }); + + assert.deepStrictEqual(patchStub.lastCall.args[0].data, { + automaticRepliesSetting: { + internalReplyMessage: `I'm out of office. Contact my manager in case of any troubles.` + } + }); + }); + + it('updates auto reply start date time of the signed-in user', async () => { + const patchStub = sinon.stub(request, 'patch').callsFake(async (opts) => { + if (opts.url === 'https://graph.microsoft.com/v1.0/me/mailboxSettings') { + return mailboxSettingsResponse; + } + + throw 'Invalid request'; + }); + + await command.action(logger, { + options: { + autoReplyStartDateTime: '2025-01-03T19:00:00.0000000' + } + }); + + assert.deepStrictEqual(patchStub.lastCall.args[0].data, { + automaticRepliesSetting: { + scheduledStartDateTime: { + dateTime: '2025-01-03T19:00:00.0000000' + } + } + }); + }); + + it('updates auto reply start time zone of the signed-in user', async () => { + const patchStub = sinon.stub(request, 'patch').callsFake(async (opts) => { + if (opts.url === 'https://graph.microsoft.com/v1.0/me/mailboxSettings') { + return mailboxSettingsResponse; + } + + throw 'Invalid request'; + }); + + await command.action(logger, { + options: { + autoReplyStartTimeZone: 'UTC' + } + }); + + assert.deepStrictEqual(patchStub.lastCall.args[0].data, { + automaticRepliesSetting: { + scheduledStartDateTime: { + timeZone: 'UTC' + } + } + }); + }); + + it('updates auto reply end date time of the signed-in user', async () => { + const patchStub = sinon.stub(request, 'patch').callsFake(async (opts) => { + if (opts.url === 'https://graph.microsoft.com/v1.0/me/mailboxSettings') { + return mailboxSettingsResponse; + } + + throw 'Invalid request'; + }); + + await command.action(logger, { + options: { + autoReplyEndDateTime: '2025-01-04T19:00:00.0000000' + } + }); + + assert.deepStrictEqual(patchStub.lastCall.args[0].data, { + automaticRepliesSetting: { + scheduledEndDateTime: { + dateTime: '2025-01-04T19:00:00.0000000' + } + } + }); + }); + + it('updates auto reply end time zone of the signed-in user', async () => { + const patchStub = sinon.stub(request, 'patch').callsFake(async (opts) => { + if (opts.url === 'https://graph.microsoft.com/v1.0/me/mailboxSettings') { + return mailboxSettingsResponse; + } + + throw 'Invalid request'; + }); + + await command.action(logger, { + options: { + autoReplyEndTimeZone: 'UTC' + } + }); + + assert.deepStrictEqual(patchStub.lastCall.args[0].data, { + automaticRepliesSetting: { + scheduledEndDateTime: { + timeZone: 'UTC' + } + } + }); + }); + + it('updates auto reply status of the signed-in user', async () => { + const patchStub = sinon.stub(request, 'patch').callsFake(async (opts) => { + if (opts.url === 'https://graph.microsoft.com/v1.0/me/mailboxSettings') { + return mailboxSettingsResponse; + } + + throw 'Invalid request'; + }); + + await command.action(logger, { + options: { + autoReplyStatus: 'scheduled' + } + }); + + assert.deepStrictEqual(patchStub.lastCall.args[0].data, { + automaticRepliesSetting: { + status: 'scheduled' + } + }); + }); + + it('fails updating mailbox settings if both userId and userName is specified in app-only mode', async () => { + sinonUtil.restore(accessToken.isAppOnlyAccessToken); + sinon.stub(accessToken, 'isAppOnlyAccessToken').returns(true); + + await assert.rejects(command.action(logger, { options: { userId: userId, userName: userName, verbose: true } }), new CommandError('When running with application permissions either userId or userName is required, but not both')); + }); + + it('fails updating mailbox settings of the signed-in user if userId is specified', async () => { + await assert.rejects(command.action(logger, { options: { userId: userId, verbose: true } }), new CommandError('You can update mailbox settings of other users only if CLI is authenticated in app-only mode')); + }); + + it('fails updating mailbox settings of the signed-in user if userName is specified', async () => { + await assert.rejects(command.action(logger, { options: { userName: userName, verbose: true } }), new CommandError('You can update mailbox settings of other users only if CLI is authenticated in app-only mode')); + }); + + it('correctly handles API OData error', async () => { + sinon.stub(request, 'patch').rejects({ + error: { + 'odata.error': { + code: '-1, InvalidOperationException', + message: { + value: 'Invalid request' + } + } + } + }); + + await assert.rejects(command.action(logger, { options: { dateFormat: 'dd.MM.yyy' } }), new CommandError('Invalid request')); + }); +}); \ No newline at end of file diff --git a/src/m365/outlook/commands/mailbox/mailbox-settings-set.ts b/src/m365/outlook/commands/mailbox/mailbox-settings-set.ts new file mode 100644 index 00000000000..d83a2b3c0f3 --- /dev/null +++ b/src/m365/outlook/commands/mailbox/mailbox-settings-set.ts @@ -0,0 +1,204 @@ +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()), + dateFormat: z.string().optional(), + timeFormat: z.string().optional(), + timeZone: z.string().optional(), + language: z.string().optional(), + delegateMeetingMessageDeliveryOptions: z.enum(['sendToDelegateAndInformationToPrincipal', 'sendToDelegateAndPrincipal', 'sendToDelegateOnly']).optional(), + workingDays: z.string().transform((value) => value.split(',')).pipe(z.enum(['monday', 'tuesday', 'wednesday', 'thursday', 'friday', 'saturday', 'sunday']).array()).optional(), + workingHoursStartTime: z.string().optional(), + workingHoursEndTime: z.string().optional(), + workingHoursTimeZone: z.string().optional(), + autoReplyExternalAudience: z.enum(['none', 'all', 'contactsOnly']).optional(), + autoReplyExternalMessage: z.string().optional(), + autoReplyInternalMessage: z.string().optional(), + autoReplyStartDateTime: z.string().optional(), + autoReplyStartTimeZone: z.string().optional(), + autoReplyEndDateTime: z.string().optional(), + autoReplyEndTimeZone: z.string().optional(), + autoReplyStatus: z.enum(['disabled', 'scheduled', 'alwaysEnabled']).optional() + }) + .strict(); + +declare type Options = z.infer; + +interface CommandArgs { + options: Options; +} + +class OutlookMailboxSettingsSetCommand extends GraphCommand { + public get name(): string { + return commands.MAILBOX_SETTINGS_SET; + } + + public get description(): string { + return 'Updates user mailbox settings'; + } + + public get schema(): z.ZodTypeAny | undefined { + return options; + } + + public getRefinedSchema(schema: typeof options): z.ZodEffects | undefined { + return schema + .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, + options.timeFormat, options.timeZone, options.dateFormat, options.delegateMeetingMessageDeliveryOptions, options.language].filter(o => o !== undefined).length > 0, { + message: 'Specify at least one of the following options: workingDays, workingHoursStartTime, workingHoursEndTime, workingHoursTimeZone' + }); + } + + public async commandAction(logger: Logger, args: CommandArgs): Promise { + const data: any = { + }; + + if (args.options.dateFormat) { + data.dateFormat = args.options.dateFormat; + } + + if (args.options.timeFormat) { + data.timeFormat = args.options.timeFormat; + } + + if (args.options.timeZone) { + data.timeZone = args.options.timeZone; + } + + if (args.options.delegateMeetingMessageDeliveryOptions) { + data.delegateMeetingMessageDeliveryOptions = args.options.delegateMeetingMessageDeliveryOptions; + } + + if (args.options.language) { + data.language = { + locale: args.options.language + }; + } + + if (args.options.workingDays || args.options.workingHoursStartTime || args.options.workingHoursEndTime || args.options.workingHoursTimeZone) { + data['workingHours'] = {}; + } + + if (args.options.workingDays) { + data.workingHours.daysOfWeek = args.options.workingDays; + } + + if (args.options.workingHoursStartTime) { + data.workingHours.startTime = args.options.workingHoursStartTime; + } + + if (args.options.workingHoursEndTime) { + data.workingHours.endTime = args.options.workingHoursEndTime; + } + + if (args.options.workingHoursTimeZone) { + data.workingHours.timeZone = { + name: args.options.workingHoursTimeZone + }; + } + + if (args.options.autoReplyStatus || args.options.autoReplyExternalAudience || args.options.autoReplyExternalMessage || args.options.autoReplyInternalMessage || args.options.autoReplyStartDateTime || args.options.autoReplyStartTimeZone || args.options.autoReplyEndDateTime || args.options.autoReplyEndTimeZone) { + data['automaticRepliesSetting'] = {}; + } + + if (args.options.autoReplyStatus) { + data.automaticRepliesSetting['status'] = args.options.autoReplyStatus; + } + + if (args.options.autoReplyExternalAudience) { + data.automaticRepliesSetting['externalAudience'] = args.options.autoReplyExternalAudience; + } + + if (args.options.autoReplyExternalMessage) { + data.automaticRepliesSetting['externalReplyMessage'] = args.options.autoReplyExternalMessage; + } + + if (args.options.autoReplyInternalMessage) { + data.automaticRepliesSetting['internalReplyMessage'] = args.options.autoReplyInternalMessage; + } + + if (args.options.autoReplyStartDateTime || args.options.autoReplyStartTimeZone) { + data.automaticRepliesSetting['scheduledStartDateTime'] = {}; + } + + if (args.options.autoReplyStartDateTime) { + data.automaticRepliesSetting['scheduledStartDateTime']['dateTime'] = args.options.autoReplyStartDateTime; + } + + if (args.options.autoReplyStartTimeZone) { + data.automaticRepliesSetting['scheduledStartDateTime']['timeZone'] = args.options.autoReplyStartTimeZone; + } + + if (args.options.autoReplyEndDateTime || args.options.autoReplyEndTimeZone) { + data.automaticRepliesSetting['scheduledEndDateTime'] = {}; + } + + if (args.options.autoReplyEndDateTime) { + data.automaticRepliesSetting['scheduledEndDateTime']['dateTime'] = args.options.autoReplyEndDateTime; + } + + if (args.options.autoReplyEndTimeZone) { + data.automaticRepliesSetting['scheduledEndDateTime']['timeZone'] = args.options.autoReplyEndTimeZone; + } + + 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, but not both'; + } + + const userIdentifier = args.options.userId ?? args.options.userName; + + if (this.verbose) { + await logger.logToStderr(`Updating mailbox settings for user ${userIdentifier}...`); + } + + requestUrl = `${this.resource}/v1.0/users('${userIdentifier}')/mailboxSettings`; + } + else { + if (args.options.userId || args.options.userName) { + throw 'You can update 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', + data: data + }; + + try { + const result = await request.patch(requestOptions); + await logger.log(result); + } + catch (err: any) { + this.handleRejectedODataJsonPromise(err); + } + } +} + +export default new OutlookMailboxSettingsSetCommand(); \ No newline at end of file From 79be24d3c1b826716eb5aed9ee9f4d36360eae8d Mon Sep 17 00:00:00 2001 From: Martin Machacek Date: Mon, 6 Jan 2025 12:30:53 +0100 Subject: [PATCH 2/6] New command: outlook mailbox settings set --- .../outlook/commands/mailbox/mailbox-settings-set.spec.ts | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) 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 d5073c1d68d..c74643074dd 100644 --- a/src/m365/outlook/commands/mailbox/mailbox-settings-set.spec.ts +++ b/src/m365/outlook/commands/mailbox/mailbox-settings-set.spec.ts @@ -95,7 +95,6 @@ describe(commands.MAILBOX_SETTINGS_SET, () => { } }; loggerLogSpy = sinon.spy(logger, 'log'); - //(command as any).pollingInterval = 0; sinon.stub(accessToken, 'isAppOnlyAccessToken').returns(false); }); @@ -263,7 +262,7 @@ describe(commands.MAILBOX_SETTINGS_SET, () => { await command.action(logger, { options: { userId: userId, - workingDays: "monday,tuesday,wednesday,thursday,friday", + workingDays: 'monday,tuesday,wednesday,thursday,friday', workingHoursStartTime: '09:00:00.000000', workingHoursEndTime: '17:00:00.000000', workingHoursTimeZone: 'UTC', @@ -292,7 +291,7 @@ describe(commands.MAILBOX_SETTINGS_SET, () => { it('updates working days of signed-in user', async () => { const patchStub = sinon.stub(request, 'patch').callsFake(async (opts) => { if (opts.url === `https://graph.microsoft.com/v1.0/me/mailboxSettings`) { - throw opts.data; + return mailboxSettingsResponse; } throw 'Invalid request'; From 1f65a89b627f537728bfed6aa09e0cb3d3ddad8d Mon Sep 17 00:00:00 2001 From: Martin Machacek Date: Mon, 6 Jan 2025 13:28:33 +0100 Subject: [PATCH 3/6] New command: outlook mailbox settings set --- src/m365/outlook/commands/mailbox/mailbox-settings-set.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/m365/outlook/commands/mailbox/mailbox-settings-set.ts b/src/m365/outlook/commands/mailbox/mailbox-settings-set.ts index d83a2b3c0f3..dfa911f7377 100644 --- a/src/m365/outlook/commands/mailbox/mailbox-settings-set.ts +++ b/src/m365/outlook/commands/mailbox/mailbox-settings-set.ts @@ -63,7 +63,7 @@ class OutlookMailboxSettingsSetCommand extends GraphCommand { options.autoReplyStatus, options.autoReplyExternalAudience, options.autoReplyExternalMessage, options.autoReplyInternalMessage, options.autoReplyStartDateTime, options.autoReplyStartTimeZone, options.autoReplyEndDateTime, options.autoReplyEndTimeZone, options.timeFormat, options.timeZone, options.dateFormat, options.delegateMeetingMessageDeliveryOptions, options.language].filter(o => o !== undefined).length > 0, { - message: 'Specify at least one of the following options: workingDays, workingHoursStartTime, workingHoursEndTime, workingHoursTimeZone' + message: 'Specify at least one of the following options: workingDays, workingHoursStartTime, workingHoursEndTime, workingHoursTimeZone, autoReplyStatus, autoReplyExternalAudience, autoReplyExternalMessage, autoReplyInternalMessage, autoReplyStartDateTime, autoReplyStartTimeZone, autoReplyEndDateTime, autoReplyEndTimeZone, timeFormat, timeZone, dateFormat, delegateMeetingMessageDeliveryOptions, or language' }); } From 58818cc9a57ed4a2d8da6f0229dd438916b175ca Mon Sep 17 00:00:00 2001 From: Martin Machacek Date: Mon, 6 Jan 2025 13:36:52 +0100 Subject: [PATCH 4/6] New command: outlook mailbox settings set --- src/config.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/config.ts b/src/config.ts index e5abd396e5a..c389ae5679c 100644 --- a/src/config.ts +++ b/src/config.ts @@ -26,6 +26,7 @@ export default { 'https://graph.microsoft.com/Mail.Read.Shared', 'https://graph.microsoft.com/Mail.ReadWrite', 'https://graph.microsoft.com/Mail.Send', + 'https://graph.microsoft.com/MailboxSettings.ReadWrite', 'https://graph.microsoft.com/Notes.ReadWrite.All', 'https://graph.microsoft.com/OnlineMeetingArtifact.Read.All', 'https://graph.microsoft.com/OnlineMeetings.ReadWrite', From 3952c9ed26ae2e9b9f6e12e71c56ffda5ca5e937 Mon Sep 17 00:00:00 2001 From: Martin Machacek Date: Tue, 7 Jan 2025 07:55:49 +0100 Subject: [PATCH 5/6] New command: outlook mailbox settings set --- .../mailbox/mailbox-settings-set.spec.ts | 174 ++++++++++-------- 1 file changed, 102 insertions(+), 72 deletions(-) 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 c74643074dd..3c08d6eeff1 100644 --- a/src/m365/outlook/commands/mailbox/mailbox-settings-set.spec.ts +++ b/src/m365/outlook/commands/mailbox/mailbox-settings-set.spec.ts @@ -222,27 +222,29 @@ describe(commands.MAILBOX_SETTINGS_SET, () => { throw 'Invalid request'; }); + const result = commandOptionsSchema.safeParse({ + dateFormat: 'dd.MM.yyy', + timeFormat: 'HH:mm', + timeZone: 'Central Europe Standard Time', + language: 'en-US', + delegateMeetingMessageDeliveryOptions: 'sendToDelegateAndInformationToPrincipal', + workingDays: 'monday,tuesday,wednesday,thursday,friday', + workingHoursStartTime: '09:00:00.000000', + workingHoursEndTime: '17:00:00.000000', + workingHoursTimeZone: 'UTC', + autoReplyExternalAudience: 'contactsOnly', + autoReplyExternalMessage: "I'm out of office", + autoReplyInternalMessage: "I'm out of office. Contact my manager in case of any troubles.", + autoReplyStartDateTime: '2025-01-06T00:00:00.0000000', + autoReplyStartTimeZone: 'UTC', + autoReplyEndDateTime: '2025-01-10T00:00:00.0000000', + autoReplyEndTimeZone: 'UTC', + autoReplyStatus: 'scheduled', + verbose: true + }); + await command.action(logger, { - options: { - dateFormat: 'dd.MM.yyy', - timeFormat: 'HH:mm', - timeZone: 'Central Europe Standard Time', - language: 'en-US', - delegateMeetingMessageDeliveryOptions: 'sendToDelegateAndInformationToPrincipal', - workingDays: 'monday,tuesday,wednesday,thursday,friday', - workingHoursStartTime: '09:00:00.000000', - workingHoursEndTime: '17:00:00.000000', - workingHoursTimeZone: 'UTC', - autoReplyExternalAudience: 'contactsOnly', - autoReplyExternalMessage: "I'm out of office", - autoReplyInternalMessage: "I'm out of office. Contact my manager in case of any troubles.", - autoReplyStartDateTime: '2025-01-06T00:00:00.0000000', - autoReplyStartTimeZone: 'UTC', - autoReplyEndDateTime: '2025-01-10T00:00:00.0000000', - autoReplyEndTimeZone: 'UTC', - autoReplyStatus: 'scheduled', - verbose: true - } + options: result.data }); assert(loggerLogSpy.calledOnceWith(mailboxSettingsResponse)); }); @@ -259,15 +261,17 @@ describe(commands.MAILBOX_SETTINGS_SET, () => { throw 'Invalid request'; }); + const result = commandOptionsSchema.safeParse({ + userId: userId, + workingDays: 'monday,tuesday,wednesday,thursday,friday', + workingHoursStartTime: '09:00:00.000000', + workingHoursEndTime: '17:00:00.000000', + workingHoursTimeZone: 'UTC', + verbose: true + }); + await command.action(logger, { - options: { - userId: userId, - workingDays: 'monday,tuesday,wednesday,thursday,friday', - workingHoursStartTime: '09:00:00.000000', - workingHoursEndTime: '17:00:00.000000', - workingHoursTimeZone: 'UTC', - verbose: true - } + options: result.data }); assert.deepStrictEqual(patchStub.lastCall.args[0].data, { @@ -297,10 +301,12 @@ describe(commands.MAILBOX_SETTINGS_SET, () => { throw 'Invalid request'; }); + const result = commandOptionsSchema.safeParse({ + workingDays: 'monday,tuesday,wednesday,thursday,friday' + }); + await command.action(logger, { - options: { - workingDays: 'monday,tuesday,wednesday,thursday,friday' - } + options: result.data }); assert.deepStrictEqual(patchStub.lastCall.args[0].data, { @@ -328,14 +334,14 @@ describe(commands.MAILBOX_SETTINGS_SET, () => { throw 'Invalid request'; }); - await command.action(logger, { - options: { - userName: userName, - workingHoursTimeZone: 'UTC', - verbose: true - } + const result = commandOptionsSchema.safeParse({ + userName: userName, + workingHoursTimeZone: 'UTC', + verbose: true }); + await command.action(logger, { options: result.data }); + assert.deepStrictEqual(patchStub.lastCall.args[0].data, { workingHours: { timeZone: { @@ -354,10 +360,12 @@ describe(commands.MAILBOX_SETTINGS_SET, () => { throw 'Invalid request'; }); + const result = commandOptionsSchema.safeParse({ + workingHoursStartTime: '07:00:00.0000000' + }); + await command.action(logger, { - options: { - workingHoursStartTime: '07:00:00.0000000' - } + options: result.data }); assert.deepStrictEqual(patchStub.lastCall.args[0].data, { @@ -376,10 +384,12 @@ describe(commands.MAILBOX_SETTINGS_SET, () => { throw 'Invalid request'; }); + const result = commandOptionsSchema.safeParse({ + workingHoursEndTime: '16:00:00.0000000' + }); + await command.action(logger, { - options: { - workingHoursEndTime: '16:00:00.0000000' - } + options: result.data }); assert.deepStrictEqual(patchStub.lastCall.args[0].data, { @@ -398,10 +408,12 @@ describe(commands.MAILBOX_SETTINGS_SET, () => { throw 'Invalid request'; }); + const result = commandOptionsSchema.safeParse({ + autoReplyExternalAudience: 'all' + }); + await command.action(logger, { - options: { - autoReplyExternalAudience: 'all' - } + options: result.data }); assert.deepStrictEqual(patchStub.lastCall.args[0].data, { @@ -420,10 +432,12 @@ describe(commands.MAILBOX_SETTINGS_SET, () => { throw 'Invalid request'; }); + const result = commandOptionsSchema.safeParse({ + autoReplyExternalMessage: `I'm out of office` + }); + await command.action(logger, { - options: { - autoReplyExternalMessage: `I'm out of office` - } + options: result.data }); assert.deepStrictEqual(patchStub.lastCall.args[0].data, { @@ -442,10 +456,12 @@ describe(commands.MAILBOX_SETTINGS_SET, () => { throw 'Invalid request'; }); + const result = commandOptionsSchema.safeParse({ + autoReplyInternalMessage: `I'm out of office. Contact my manager in case of any troubles.` + }); + await command.action(logger, { - options: { - autoReplyInternalMessage: `I'm out of office. Contact my manager in case of any troubles.` - } + options: result.data }); assert.deepStrictEqual(patchStub.lastCall.args[0].data, { @@ -464,10 +480,12 @@ describe(commands.MAILBOX_SETTINGS_SET, () => { throw 'Invalid request'; }); + const result = commandOptionsSchema.safeParse({ + autoReplyStartDateTime: '2025-01-03T19:00:00.0000000' + }); + await command.action(logger, { - options: { - autoReplyStartDateTime: '2025-01-03T19:00:00.0000000' - } + options: result.data }); assert.deepStrictEqual(patchStub.lastCall.args[0].data, { @@ -488,10 +506,12 @@ describe(commands.MAILBOX_SETTINGS_SET, () => { throw 'Invalid request'; }); + const result = commandOptionsSchema.safeParse({ + autoReplyStartTimeZone: 'UTC' + }); + await command.action(logger, { - options: { - autoReplyStartTimeZone: 'UTC' - } + options: result.data }); assert.deepStrictEqual(patchStub.lastCall.args[0].data, { @@ -512,10 +532,12 @@ describe(commands.MAILBOX_SETTINGS_SET, () => { throw 'Invalid request'; }); + const result = commandOptionsSchema.safeParse({ + autoReplyEndDateTime: '2025-01-04T19:00:00.0000000' + }); + await command.action(logger, { - options: { - autoReplyEndDateTime: '2025-01-04T19:00:00.0000000' - } + options: result.data }); assert.deepStrictEqual(patchStub.lastCall.args[0].data, { @@ -536,10 +558,12 @@ describe(commands.MAILBOX_SETTINGS_SET, () => { throw 'Invalid request'; }); + const result = commandOptionsSchema.safeParse({ + autoReplyEndTimeZone: 'UTC' + }); + await command.action(logger, { - options: { - autoReplyEndTimeZone: 'UTC' - } + options: result.data }); assert.deepStrictEqual(patchStub.lastCall.args[0].data, { @@ -560,10 +584,12 @@ describe(commands.MAILBOX_SETTINGS_SET, () => { throw 'Invalid request'; }); + const result = commandOptionsSchema.safeParse({ + autoReplyStatus: 'scheduled' + }); + await command.action(logger, { - options: { - autoReplyStatus: 'scheduled' - } + options: result.data }); assert.deepStrictEqual(patchStub.lastCall.args[0].data, { @@ -577,15 +603,19 @@ describe(commands.MAILBOX_SETTINGS_SET, () => { sinonUtil.restore(accessToken.isAppOnlyAccessToken); sinon.stub(accessToken, 'isAppOnlyAccessToken').returns(true); - await assert.rejects(command.action(logger, { options: { userId: userId, userName: userName, verbose: true } }), new CommandError('When running with application permissions either userId or userName is required, but not both')); + const result = commandOptionsSchema.safeParse({ userId: userId, userName: userName, 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')); }); it('fails updating mailbox settings of the signed-in user if userId is specified', async () => { - await assert.rejects(command.action(logger, { options: { userId: userId, verbose: true } }), new CommandError('You can update mailbox settings of other users only if CLI is authenticated in app-only mode')); + const result = commandOptionsSchema.safeParse({ userId: userId, timeFormat: 'HH:mm', verbose: true }); + await assert.rejects(command.action(logger, { options: result.data }), new CommandError('You can update mailbox settings of other users only if CLI is authenticated in app-only mode')); }); it('fails updating mailbox settings of the signed-in user if userName is specified', async () => { - await assert.rejects(command.action(logger, { options: { userName: userName, verbose: true } }), new CommandError('You can update mailbox settings of other users only if CLI is authenticated in app-only mode')); + const result = commandOptionsSchema.safeParse({ userName: userName, timeFormat: 'HH:mm', verbose: true }); + await assert.rejects(command.action(logger, { options: result.data }), new CommandError('You can update mailbox settings of other users only if CLI is authenticated in app-only mode')); }); it('correctly handles API OData error', async () => { @@ -599,7 +629,7 @@ describe(commands.MAILBOX_SETTINGS_SET, () => { } } }); - - await assert.rejects(command.action(logger, { options: { dateFormat: 'dd.MM.yyy' } }), new CommandError('Invalid request')); + const result = commandOptionsSchema.safeParse({ dateFormat: 'dd.MM.yyy' }); + await assert.rejects(command.action(logger, { options: result.data }), new CommandError('Invalid request')); }); }); \ No newline at end of file From 1a747173fdb79fefbfa2a8ca8d547b8eed661f95 Mon Sep 17 00:00:00 2001 From: Martin Machacek Date: Fri, 10 Jan 2025 20:53:59 +0100 Subject: [PATCH 6/6] New command: outlook mailbox settings set --- .../outlook/mailbox/mailbox-settings-set.mdx | 14 ++-- .../commands/mailbox/mailbox-settings-set.ts | 82 ++++++++++--------- 2 files changed, 50 insertions(+), 46 deletions(-) diff --git a/docs/docs/cmd/outlook/mailbox/mailbox-settings-set.mdx b/docs/docs/cmd/outlook/mailbox/mailbox-settings-set.mdx index 47069811fbc..71930250329 100644 --- a/docs/docs/cmd/outlook/mailbox/mailbox-settings-set.mdx +++ b/docs/docs/cmd/outlook/mailbox/mailbox-settings-set.mdx @@ -16,10 +16,10 @@ m365 outlook mailbox settings set [options] ```md definition-list `-i, --userId [userId]` -: The ID of the Microsoft Entra user to update mailbox settings for. Specify either `userId` or `userName`, but not both. This option is required when using application permissions. +: The ID of the Microsoft Entra user to update mailbox settings for. Either `userId` or `userName` is required when using application permissions. `-n, --userName [userName]` -: The UPN of the Microsoft Entra user to update mailbox settings for. Specify either `userId` or `userName`, but not both. This option is required when using application permissions. +: The UPN of the Microsoft Entra user to update mailbox settings for. Either `userId` or `userName` is required when using application permissions. `--dateFormat [dateFormat]` : The date format for the user's mailbox. Example: `dd.MM.yyyy`. @@ -28,7 +28,7 @@ m365 outlook mailbox settings set [options] : The time format for the user's mailbox. Example: `H:mm`. `--timeZone [timeZone]` -: The default time zone for the user's mailbox. Should follow [Windows time zone name](https://learn.microsoft.com/en-us/windows-hardware/manufacture/desktop/default-time-zones?view=windows-11#time-zones) or [IANA time zone identifier](https://en.wikipedia.org/wiki/List_of_tz_database_time_zones#List). Example: `Central Europe Standard Time`. +: The default time zone for the user's mailbox. Should follow [Windows time zone name](https://learn.microsoft.com/windows-hardware/manufacture/desktop/default-time-zones?view=windows-11#time-zones) or [IANA time zone identifier](https://en.wikipedia.org/wiki/List_of_tz_database_time_zones#List). Example: `Central Europe Standard Time`. `--language [language]` : The preferred language for the user. Should follow [ISO 639-1 Code](https://learn.microsoft.com/openspecs/office_standards/ms-oe376/6c085406-a698-4e12-9d4d-c3b0ee3dbc4a). Example: `en-US`. @@ -40,13 +40,13 @@ m365 outlook mailbox settings set [options] : The days of the week on which the user works, separated by a comma. Allowed values are `monday`, `tuesday`, `wednesday`, `thursday`, `friday`, `saturday`, or `sunday`. `--workingHoursStartTime [workingHoursStartTime]` -: The time of the day that the user starts working. Example: `09:00:00.000000`. +: The time of the day that the user starts working. Example: `09:00`. `--workingHoursEndTime [workingHoursEndTime]` -: The time of the day that the user stops working. Example: `17:00:00.000000`. +: The time of the day that the user stops working. Example: `17:00`. `--workingHoursTimeZone [workingHoursTimeZone]` -: The name of a time zone to which the working hours apply. Should follow [Windows time zone name](https://learn.microsoft.com/en-us/windows-hardware/manufacture/desktop/default-time-zones?view=windows-11#time-zones) or [IANA time zone identifier](https://en.wikipedia.org/wiki/List_of_tz_database_time_zones#List). Example: `Central Europe Standard Time`. +: The name of a time zone to which the working hours apply. Should follow [Windows time zone name](https://learn.microsoft.com/windows-hardware/manufacture/desktop/default-time-zones?view=windows-11#time-zones) or [IANA time zone identifier](https://en.wikipedia.org/wiki/List_of_tz_database_time_zones#List). Example: `Central Europe Standard Time`. `--autoReplyExternalAudience [autoReplyExternalAudience]` : Specifies external audience who will receive reply message. Allowed values are `none`, `contactsOnly`, or `all`. @@ -86,7 +86,7 @@ m365 outlook mailbox settings set --dateFormat 'dd.MM.yyyy' --timeFormat 'H:mm' Update working hours of a user specified by id ```sh -m365 outlook mailbox settings set --userId 1caf7dcd-7e83-4c3a-94f7-932a1299c844 --workingDays 'monday,tuesday,thursday,friday' --workingHoursStartTime '08:00:00.0000000' --workingHoursEndTime '16:30:00.0000000' --workingHoursTimeZone 'Central Europe Standard Time' +m365 outlook mailbox settings set --userId 1caf7dcd-7e83-4c3a-94f7-932a1299c844 --workingDays 'monday,tuesday,thursday,friday' --workingHoursStartTime '08:00' --workingHoursEndTime '16:30' --workingHoursTimeZone 'Central Europe Standard Time' ``` Set scheduled automatic replies for the internal audience of a user specified by UPN diff --git a/src/m365/outlook/commands/mailbox/mailbox-settings-set.ts b/src/m365/outlook/commands/mailbox/mailbox-settings-set.ts index dfa911f7377..948633d3700 100644 --- a/src/m365/outlook/commands/mailbox/mailbox-settings-set.ts +++ b/src/m365/outlook/commands/mailbox/mailbox-settings-set.ts @@ -68,6 +68,48 @@ class OutlookMailboxSettingsSetCommand extends GraphCommand { } 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, but not both'; + } + + const userIdentifier = args.options.userId ?? args.options.userName; + + if (this.verbose) { + await logger.logToStderr(`Updating mailbox settings for user ${userIdentifier}...`); + } + + requestUrl = `${this.resource}/v1.0/users('${userIdentifier}')/mailboxSettings`; + } + else { + if (args.options.userId || args.options.userName) { + throw 'You can update 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', + data: this.createRequestBody(args) + }; + + try { + const result = await request.patch(requestOptions); + await logger.log(result); + } + catch (err: any) { + this.handleRejectedODataJsonPromise(err); + } + } + + private createRequestBody(args: CommandArgs): any { const data: any = { }; @@ -159,45 +201,7 @@ class OutlookMailboxSettingsSetCommand extends GraphCommand { data.automaticRepliesSetting['scheduledEndDateTime']['timeZone'] = args.options.autoReplyEndTimeZone; } - 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, but not both'; - } - - const userIdentifier = args.options.userId ?? args.options.userName; - - if (this.verbose) { - await logger.logToStderr(`Updating mailbox settings for user ${userIdentifier}...`); - } - - requestUrl = `${this.resource}/v1.0/users('${userIdentifier}')/mailboxSettings`; - } - else { - if (args.options.userId || args.options.userName) { - throw 'You can update 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', - data: data - }; - - try { - const result = await request.patch(requestOptions); - await logger.log(result); - } - catch (err: any) { - this.handleRejectedODataJsonPromise(err); - } + return data; } }