Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
69 changes: 42 additions & 27 deletions src/m365/outlook/commands/mail/mail-send.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,12 +14,13 @@ 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 from './mail-send.js';
import command, { options } from './mail-send.js';

describe(commands.MAIL_SEND, () => {
let log: string[];
let logger: Logger;
let commandInfo: CommandInfo;
let commandOptionsSchema: typeof options;

before(() => {
sinon.stub(auth, 'restoreAuth').resolves();
Expand All @@ -32,6 +33,7 @@ describe(commands.MAIL_SEND, () => {
accessToken: 'abc'
};
commandInfo = cli.getCommandInfo(command);
commandOptionsSchema = commandInfo.command.getSchemaToParse() as typeof options;
});

beforeEach(() => {
Expand Down Expand Up @@ -311,14 +313,18 @@ describe(commands.MAIL_SEND, () => {
new CommandError(`An error has occurred`));
});

it('fails validation if bodyContentType is invalid', async () => {
const actual = await command.validate({ options: { subject: 'Lorem ipsum', to: 'mail@domain.com', bodyContents: 'Lorem ipsum', bodyContentType: 'Invalid' } }, commandInfo);
assert.notStrictEqual(actual, true);
it('defines schema', () => {
assert.notStrictEqual(command.schema, undefined);
});

it('fails validation if importance is invalid', async () => {
const actual = await command.validate({ options: { subject: 'Lorem ipsum', to: 'mail@domain.com', bodyContents: 'Lorem ipsum', importance: 'Invalid' } }, commandInfo);
assert.notStrictEqual(actual, true);
it('fails validation if bodyContentType is invalid', () => {
const actual = commandOptionsSchema.safeParse({ subject: 'Lorem ipsum', to: 'mail@domain.com', bodyContents: 'Lorem ipsum', bodyContentType: 'Invalid' });
assert.notStrictEqual(actual.success, true);
});

it('fails validation if importance is invalid', () => {
const actual = commandOptionsSchema.safeParse({ subject: 'Lorem ipsum', to: 'mail@domain.com', bodyContents: 'Lorem ipsum', importance: 'Invalid' });
assert.notStrictEqual(actual.success, true);
});

it('fails validation if file doesn\'t exist', async () => {
Expand Down Expand Up @@ -364,39 +370,48 @@ describe(commands.MAIL_SEND, () => {
assert.notStrictEqual(actual, true);
});

it('passes validation when subject, to and bodyContents are specified', async () => {
const actual = await command.validate({ options: { subject: 'Lorem ipsum', to: 'mail@domain.com', bodyContents: 'Lorem ipsum' } }, commandInfo);
it('passes validation when valid attachments are specified', async () => {
sinon.stub(fs, 'existsSync').returns(true);
sinon.stub(fs, 'lstatSync').returns({ isFile: () => true } as any);
sinon.stub(fs, 'readFileSync').returns('file content');

const actual = await command.validate({ options: { subject: 'Lorem ipsum', to: 'mail@domain.com', bodyContents: 'Lorem ipsum', attachment: 'C:/File.txt' } }, commandInfo);
assert.strictEqual(actual, true);
});

it('passes validation when multiple to emails are specified', async () => {
const actual = await command.validate({ options: { subject: 'Lorem ipsum', to: 'mail@domain.com,mail2@domain.com', bodyContents: 'Lorem ipsum' } }, commandInfo);
assert.strictEqual(actual, true);
it('passes validation when subject, to and bodyContents are specified', () => {
const actual = commandOptionsSchema.safeParse({ subject: 'Lorem ipsum', to: 'mail@domain.com', bodyContents: 'Lorem ipsum' });
assert.strictEqual(actual.success, true);
});

it('passes validation when multiple to emails separated with command and space are specified', async () => {
const actual = await command.validate({ options: { subject: 'Lorem ipsum', to: 'mail@domain.com, mail2@domain.com', bodyContents: 'Lorem ipsum' } }, commandInfo);
assert.strictEqual(actual, true);
it('passes validation when multiple to emails are specified', () => {
const actual = commandOptionsSchema.safeParse({ subject: 'Lorem ipsum', to: 'mail@domain.com,mail2@domain.com', bodyContents: 'Lorem ipsum' });
assert.strictEqual(actual.success, true);
});

it('passes validation when bodyContentType is set to Text', async () => {
const actual = await command.validate({ options: { subject: 'Lorem ipsum', to: 'mail@domain.com', bodyContents: 'Lorem ipsum', bodyContentType: 'Text' } }, commandInfo);
assert.strictEqual(actual, true);
it('passes validation when multiple to emails separated with command and space are specified', () => {
const actual = commandOptionsSchema.safeParse({ subject: 'Lorem ipsum', to: 'mail@domain.com, mail2@domain.com', bodyContents: 'Lorem ipsum' });
assert.strictEqual(actual.success, true);
});

it('passes validation when bodyContentType is set to HTML', async () => {
const actual = await command.validate({ options: { subject: 'Lorem ipsum', to: 'mail@domain.com', bodyContents: 'Lorem ipsum', bodyContentType: 'HTML' } }, commandInfo);
assert.strictEqual(actual, true);
it('passes validation when bodyContentType is set to Text', () => {
const actual = commandOptionsSchema.safeParse({ subject: 'Lorem ipsum', to: 'mail@domain.com', bodyContents: 'Lorem ipsum', bodyContentType: 'Text' });
assert.strictEqual(actual.success, true);
});

it('passes validation when saveToSentItems is set to false', async () => {
const actual = await command.validate({ options: { subject: 'Lorem ipsum', to: 'mail@domain.com', bodyContents: 'Lorem ipsum', saveToSentItems: false } }, commandInfo);
assert.strictEqual(actual, true);
it('passes validation when bodyContentType is set to HTML', () => {
const actual = commandOptionsSchema.safeParse({ subject: 'Lorem ipsum', to: 'mail@domain.com', bodyContents: 'Lorem ipsum', bodyContentType: 'HTML' });
assert.strictEqual(actual.success, true);
});

it('passes validation when saveToSentItems is set to true', async () => {
const actual = await command.validate({ options: { subject: 'Lorem ipsum', to: 'mail@domain.com', bodyContents: 'Lorem ipsum', saveToSentItems: true } }, commandInfo);
assert.strictEqual(actual, true);
it('passes validation when saveToSentItems is set to false', () => {
const actual = commandOptionsSchema.safeParse({ subject: 'Lorem ipsum', to: 'mail@domain.com', bodyContents: 'Lorem ipsum', saveToSentItems: false });
assert.strictEqual(actual.success, true);
});

it('passes validation when saveToSentItems is set to true', () => {
const actual = commandOptionsSchema.safeParse({ subject: 'Lorem ipsum', to: 'mail@domain.com', bodyContents: 'Lorem ipsum', saveToSentItems: true });
assert.strictEqual(actual.success, true);
});

it('sends email using a specified group mailbox', async () => {
Expand Down
111 changes: 23 additions & 88 deletions src/m365/outlook/commands/mail/mail-send.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,31 +2,35 @@ import fs from 'fs';
import path from 'path';
import auth from '../../../../Auth.js';
import { Logger } from '../../../../cli/Logger.js';
import GlobalOptions from '../../../../GlobalOptions.js';
import { globalOptionsZod } from '../../../../Command.js';
import request, { CliRequestOptions } from '../../../../request.js';
import { accessToken } from '../../../../utils/accessToken.js';
import { formatting } from '../../../../utils/formatting.js';
import GraphCommand from '../../../base/GraphCommand.js';
import commands from '../../commands.js';
import { z } from 'zod';

export const options = z.strictObject({
...globalOptionsZod.shape,
subject: z.string().alias('s'),
to: z.string().alias('t'),
cc: z.string().optional(),
bcc: z.string().optional(),
sender: z.string().optional(),
mailbox: z.string().optional().alias('m'),
bodyContents: z.string(),
bodyContentType: z.enum(['Text', 'HTML']).optional(),
importance: z.enum(['low', 'normal', 'high']).optional(),
attachment: z.union([z.string(), z.string().array()]).optional(),
saveToSentItems: z.boolean().optional()
});

declare type Options = z.infer<typeof options>;

interface CommandArgs {
options: Options;
}

interface Options extends GlobalOptions {
subject: string;
to: string;
cc?: string;
bcc?: string;
sender?: string;
mailbox?: string;
bodyContents: string;
bodyContentType?: string;
importance?: string;
attachment?: string | string[];
saveToSentItems?: boolean;
}

class OutlookMailSendCommand extends GraphCommand {
public get name(): string {
return commands.MAIL_SEND;
Expand All @@ -36,88 +40,19 @@ class OutlookMailSendCommand extends GraphCommand {
return 'Sends an email';
}

public get schema(): z.ZodType | undefined {
return options;
}

constructor() {
super();

this.#initTelemetry();
this.#initOptions();
this.#initTypes();
this.#initValidators();
}

#initTelemetry(): void {
this.telemetry.push((args: CommandArgs) => {
Object.assign(this.telemetryProperties, {
cc: typeof args.options.cc !== 'undefined',
bcc: typeof args.options.bcc !== 'undefined',
bodyContentType: args.options.bodyContentType,
saveToSentItems: args.options.saveToSentItems,
importance: args.options.importance,
mailbox: typeof args.options.mailbox !== 'undefined',
sender: typeof args.options.sender !== 'undefined',
attachment: typeof args.options.attachment !== 'undefined'
});
});
}

#initOptions(): void {
this.options.unshift(
{
option: '-s, --subject <subject>'
},
{
option: '-t, --to <to>'
},
{
option: '--cc [cc]'
},
{
option: '--bcc [bcc]'
},
{
option: '--sender [sender]'
},
{
option: '-m, --mailbox [mailbox]'
},
{
option: '--bodyContents <bodyContents>'
},
{
option: '--bodyContentType [bodyContentType]',
autocomplete: ['Text', 'HTML']
},
{
option: '--importance [importance]',
autocomplete: ['low', 'normal', 'high']
},
{
option: '--attachment [attachment]'
},
{
option: '--saveToSentItems [saveToSentItems]',
autocomplete: ['true', 'false']
}
);
}

#initTypes(): void {
this.types.boolean.push('saveToSentItems');
}

#initValidators(): void {
this.validators.push(
async (args: CommandArgs) => {
if (args.options.bodyContentType &&
args.options.bodyContentType !== 'Text' &&
args.options.bodyContentType !== 'HTML') {
return `${args.options.bodyContentType} is not a valid value for the bodyContentType option. Allowed values are Text|HTML`;
}

if (args.options.importance && ['low', 'normal', 'high'].indexOf(args.options.importance) === -1) {
return `'${args.options.importance}' is not a valid value for the importance option. Allowed values are low|normal|high`;
}

if (args.options.attachment) {
const attachments: string[] = typeof args.options.attachment === 'string' ? [args.options.attachment] : args.options.attachment;

Expand Down
32 changes: 31 additions & 1 deletion src/m365/outlook/commands/message/message-get.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ 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 from './message-get.js';
import command, { options } from './message-get.js';
import { settingsNames } from '../../../../settingsNames.js';

describe(commands.MESSAGE_GET, () => {
Expand Down Expand Up @@ -77,6 +77,7 @@ describe(commands.MESSAGE_GET, () => {
let logger: Logger;
let loggerLogSpy: sinon.SinonSpy;
let commandInfo: CommandInfo;
let commandOptionsSchema: typeof options;

before(() => {
sinon.stub(auth, 'restoreAuth').resolves();
Expand All @@ -89,6 +90,7 @@ describe(commands.MESSAGE_GET, () => {
accessToken: 'abc'
};
commandInfo = cli.getCommandInfo(command);
commandOptionsSchema = commandInfo.command.getSchemaToParse() as typeof options;
});

beforeEach(() => {
Expand Down Expand Up @@ -260,6 +262,34 @@ describe(commands.MESSAGE_GET, () => {
assert.equal(actual, true);
});

it('defines schema', () => {
assert.notStrictEqual(command.schema, undefined);
});

it('defines refined schema', () => {
assert.notStrictEqual(command.getRefinedSchema(command.schema as any), undefined);
});

it('fails validation if userId is not a valid GUID', () => {
const actual = commandOptionsSchema.safeParse({ id: messageId, userId: 'invalid-guid' });
assert.strictEqual(actual.success, false);
});

it('fails validation if userName is not a valid UPN', () => {
const actual = commandOptionsSchema.safeParse({ id: messageId, userName: 'invalid-upn' });
assert.strictEqual(actual.success, false);
});

it('fails validation if both userId and userName are specified', () => {
const actual = commandOptionsSchema.safeParse({ id: messageId, userId: userId, userName: userName });
assert.strictEqual(actual.success, false);
});

it('passes validation if only userId is specified', () => {
const actual = commandOptionsSchema.safeParse({ id: messageId, userId: userId });
assert.strictEqual(actual.success, true);
});

it('throws an error when the upn or userName is not defined when signed in using app only authentication', async () => {
sinonUtil.restore([accessToken.isAppOnlyAccessToken]);
sinon.stub(accessToken, 'isAppOnlyAccessToken').returns(true);
Expand Down
Loading
Loading