From 5290e0a63beb5ec5f134c996c7e25e3814cbce4f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=9C=A8=20Audrey=20=E2=9C=A8?= Date: Tue, 22 Jul 2025 09:33:34 -0400 Subject: [PATCH] [PM-19054] configure send with email otp authentication via cli (#15360) --- apps/cli/src/commands/get.command.ts | 10 +- .../src/tools/send/commands/create.command.ts | 10 +- .../src/tools/send/commands/edit.command.ts | 24 ++- apps/cli/src/tools/send/commands/index.ts | 1 + .../tools/send/commands/template.command.ts | 35 ++++ .../src/tools/send/models/send.response.ts | 3 + apps/cli/src/tools/send/send.program.ts | 64 +++--- apps/cli/src/tools/send/util.spec.ts | 194 ++++++++++++++++++ apps/cli/src/tools/send/util.ts | 55 +++++ .../src/tools/send/models/data/send.data.ts | 2 + .../src/tools/send/models/domain/send.spec.ts | 6 +- .../src/tools/send/models/domain/send.ts | 2 + .../tools/send/models/request/send.request.ts | 2 + .../send/models/response/send.response.ts | 2 + .../src/tools/send/models/view/send.view.ts | 1 + .../src/tools/send/services/send.service.ts | 7 +- 16 files changed, 369 insertions(+), 49 deletions(-) create mode 100644 apps/cli/src/tools/send/commands/template.command.ts create mode 100644 apps/cli/src/tools/send/util.spec.ts create mode 100644 apps/cli/src/tools/send/util.ts diff --git a/apps/cli/src/commands/get.command.ts b/apps/cli/src/commands/get.command.ts index aa2db7c81a..b20052fbb5 100644 --- a/apps/cli/src/commands/get.command.ts +++ b/apps/cli/src/commands/get.command.ts @@ -25,7 +25,6 @@ import { LoginExport } from "@bitwarden/common/models/export/login.export"; import { SecureNoteExport } from "@bitwarden/common/models/export/secure-note.export"; import { ErrorResponse } from "@bitwarden/common/models/response/error.response"; import { Utils } from "@bitwarden/common/platform/misc/utils"; -import { SendType } from "@bitwarden/common/tools/send/enums/send-type"; import { CipherId, OrganizationId, UserId } from "@bitwarden/common/types/guid"; import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; import { FolderService } from "@bitwarden/common/vault/abstractions/folder/folder.service.abstraction"; @@ -44,7 +43,6 @@ import { SelectionReadOnly } from "../admin-console/models/selection-read-only"; import { Response } from "../models/response"; import { StringResponse } from "../models/response/string.response"; import { TemplateResponse } from "../models/response/template.response"; -import { SendResponse } from "../tools/send/models/send.response"; import { CliUtils } from "../utils"; import { CipherResponse } from "../vault/models/cipher.response"; import { FolderResponse } from "../vault/models/folder.response"; @@ -577,11 +575,11 @@ export class GetCommand extends DownloadCommand { case "org-collection": template = OrganizationCollectionRequest.template(); break; - case "send.text": - template = SendResponse.template(SendType.Text); - break; case "send.file": - template = SendResponse.template(SendType.File); + case "send.text": + template = Response.badRequest( + `Invalid template object. Use \`bw send template ${id}\` instead.`, + ); break; default: return Response.badRequest("Unknown template object."); diff --git a/apps/cli/src/tools/send/commands/create.command.ts b/apps/cli/src/tools/send/commands/create.command.ts index a9264c5012..d4f544d39b 100644 --- a/apps/cli/src/tools/send/commands/create.command.ts +++ b/apps/cli/src/tools/send/commands/create.command.ts @@ -76,9 +76,14 @@ export class SendCreateCommand { const filePath = req.file?.fileName ?? options.file; const text = req.text?.text ?? options.text; const hidden = req.text?.hidden ?? options.hidden; - const password = req.password ?? options.password; + const password = req.password ?? options.password ?? undefined; + const emails = req.emails ?? options.emails ?? undefined; const maxAccessCount = req.maxAccessCount ?? options.maxAccessCount; + if (emails !== undefined && password !== undefined) { + return Response.badRequest("--password and --emails are mutually exclusive."); + } + req.key = null; req.maxAccessCount = maxAccessCount; @@ -133,6 +138,7 @@ export class SendCreateCommand { // Add dates from template encSend.deletionDate = sendView.deletionDate; encSend.expirationDate = sendView.expirationDate; + encSend.emails = emails && emails.join(","); await this.sendApiService.save([encSend, fileData]); const newSend = await this.sendService.getFromState(encSend.id); @@ -151,12 +157,14 @@ class Options { text: string; maxAccessCount: number; password: string; + emails: Array; hidden: boolean; constructor(passedOptions: Record) { this.file = passedOptions?.file; this.text = passedOptions?.text; this.password = passedOptions?.password; + this.emails = passedOptions?.email; this.hidden = CliUtils.convertBooleanOption(passedOptions?.hidden); this.maxAccessCount = passedOptions?.maxAccessCount != null ? parseInt(passedOptions.maxAccessCount, null) : null; diff --git a/apps/cli/src/tools/send/commands/edit.command.ts b/apps/cli/src/tools/send/commands/edit.command.ts index ed719b5831..09f89041cc 100644 --- a/apps/cli/src/tools/send/commands/edit.command.ts +++ b/apps/cli/src/tools/send/commands/edit.command.ts @@ -50,11 +50,21 @@ export class SendEditCommand { const normalizedOptions = new Options(cmdOptions); req.id = normalizedOptions.itemId || req.id; - - if (req.id != null) { - req.id = req.id.toLowerCase(); + if (normalizedOptions.emails) { + req.emails = normalizedOptions.emails; + req.password = undefined; + } else if (normalizedOptions.password) { + req.emails = undefined; + req.password = normalizedOptions.password; + } else if (req.password && (typeof req.password !== "string" || req.password === "")) { + req.password = undefined; } + if (!req.id) { + return Response.error("`itemid` was not provided."); + } + + req.id = req.id.toLowerCase(); const send = await this.sendService.getFromState(req.id); if (send == null) { @@ -76,10 +86,6 @@ export class SendEditCommand { let sendView = await send.decrypt(); sendView = SendResponse.toView(req, sendView); - if (typeof req.password !== "string" || req.password === "") { - req.password = null; - } - try { const [encSend, encFileData] = await this.sendService.encrypt(sendView, null, req.password); // Add dates from template @@ -97,8 +103,12 @@ export class SendEditCommand { class Options { itemId: string; + password: string; + emails: string[]; constructor(passedOptions: Record) { this.itemId = passedOptions?.itemId || passedOptions?.itemid; + this.password = passedOptions.password; + this.emails = passedOptions.email; } } diff --git a/apps/cli/src/tools/send/commands/index.ts b/apps/cli/src/tools/send/commands/index.ts index 645f5c0d1d..452c228dd9 100644 --- a/apps/cli/src/tools/send/commands/index.ts +++ b/apps/cli/src/tools/send/commands/index.ts @@ -5,3 +5,4 @@ export * from "./get.command"; export * from "./list.command"; export * from "./receive.command"; export * from "./remove-password.command"; +export * from "./template.command"; diff --git a/apps/cli/src/tools/send/commands/template.command.ts b/apps/cli/src/tools/send/commands/template.command.ts new file mode 100644 index 0000000000..c1c2c97b03 --- /dev/null +++ b/apps/cli/src/tools/send/commands/template.command.ts @@ -0,0 +1,35 @@ +import { SendType } from "@bitwarden/common/tools/send/enums/send-type"; + +import { Response } from "../../../models/response"; +import { TemplateResponse } from "../../../models/response/template.response"; +import { SendResponse } from "../models/send.response"; + +export class SendTemplateCommand { + constructor() {} + + run(type: string): Response { + let template: SendResponse | undefined; + let response: Response; + + switch (type) { + case "send.text": + case "text": + template = SendResponse.template(SendType.Text); + break; + case "send.file": + case "file": + template = SendResponse.template(SendType.File); + break; + default: + response = Response.badRequest("Unknown template object."); + } + + if (template) { + response = Response.success(new TemplateResponse(template)); + } + + response ??= Response.badRequest("An error occurred while retrieving the template."); + + return response; + } +} diff --git a/apps/cli/src/tools/send/models/send.response.ts b/apps/cli/src/tools/send/models/send.response.ts index 4d680b5c0a..a0c1d3f83c 100644 --- a/apps/cli/src/tools/send/models/send.response.ts +++ b/apps/cli/src/tools/send/models/send.response.ts @@ -26,6 +26,7 @@ export class SendResponse implements BaseResponse { req.deletionDate = this.getStandardDeletionDate(deleteInDays); req.expirationDate = null; req.password = null; + req.emails = null; req.disabled = false; req.hideEmail = false; return req; @@ -50,6 +51,7 @@ export class SendResponse implements BaseResponse { view.deletionDate = send.deletionDate; view.expirationDate = send.expirationDate; view.password = send.password; + view.emails = send.emails ?? []; view.disabled = send.disabled; view.hideEmail = send.hideEmail; return view; @@ -87,6 +89,7 @@ export class SendResponse implements BaseResponse { expirationDate: Date; password: string; passwordSet: boolean; + emails?: Array; disabled: boolean; hideEmail: boolean; diff --git a/apps/cli/src/tools/send/send.program.ts b/apps/cli/src/tools/send/send.program.ts index cbeda188a9..650f448e55 100644 --- a/apps/cli/src/tools/send/send.program.ts +++ b/apps/cli/src/tools/send/send.program.ts @@ -4,13 +4,12 @@ import * as fs from "fs"; import * as path from "path"; import * as chalk from "chalk"; -import { program, Command, OptionValues } from "commander"; +import { program, Command, Option, OptionValues } from "commander"; import { Utils } from "@bitwarden/common/platform/misc/utils"; import { SendType } from "@bitwarden/common/tools/send/enums/send-type"; import { BaseProgram } from "../../base-program"; -import { GetCommand } from "../../commands/get.command"; import { Response } from "../../models/response"; import { CliUtils } from "../../utils"; @@ -22,10 +21,12 @@ import { SendListCommand, SendReceiveCommand, SendRemovePasswordCommand, + SendTemplateCommand, } from "./commands"; import { SendFileResponse } from "./models/send-file.response"; import { SendTextResponse } from "./models/send-text.response"; import { SendResponse } from "./models/send.response"; +import { parseEmail } from "./util"; const writeLn = CliUtils.writeLn; @@ -48,6 +49,17 @@ export class SendProgram extends BaseProgram { "The number of days in the future to set deletion date, defaults to 7", "7", ) + .addOption( + new Option( + "--password ", + "optional password to access this Send. Can also be specified in JSON.", + ).conflicts("email"), + ) + .option( + "--email ", + "optional emails to access this Send. Can also be specified in JSON.", + parseEmail, + ) .option("-a, --maxAccessCount ", "The amount of max possible accesses.") .option("--hidden", "Hide in web by default. Valid only if --file is not set.") .option( @@ -139,26 +151,9 @@ export class SendProgram extends BaseProgram { return new Command("template") .argument("", "Valid objects are: send.text, send.file") .description("Get json templates for send objects") - .action(async (object) => { - const cmd = new GetCommand( - this.serviceContainer.cipherService, - this.serviceContainer.folderService, - this.serviceContainer.collectionService, - this.serviceContainer.totpService, - this.serviceContainer.auditService, - this.serviceContainer.keyService, - this.serviceContainer.encryptService, - this.serviceContainer.searchService, - this.serviceContainer.apiService, - this.serviceContainer.organizationService, - this.serviceContainer.eventCollectionService, - this.serviceContainer.billingAccountProfileStateService, - this.serviceContainer.accountService, - this.serviceContainer.cliRestrictedItemTypesService, - ); - const response = await cmd.run("template", object, null); - this.processResponse(response); - }); + .action((options: OptionValues) => + this.processResponse(new SendTemplateCommand().run(options.object)), + ); } private getCommand(): Command { @@ -208,10 +203,6 @@ export class SendProgram extends BaseProgram { .option("--file ", "file to Send. Can also be specified in parent's JSON.") .option("--text ", "text to Send. Can also be specified in parent's JSON.") .option("--hidden", "text hidden flag. Valid only with the --text option.") - .option( - "--password ", - "optional password to access this Send. Can also be specified in JSON", - ) .on("--help", () => { writeLn(""); writeLn("Note:"); @@ -219,13 +210,13 @@ export class SendProgram extends BaseProgram { writeLn("", true); }) .action(async (encodedJson: string, options: OptionValues, args: { parent: Command }) => { - // Work-around to support `--fullObject` option for `send create --fullObject` - // Calling `option('--fullObject', ...)` above won't work due to Commander doesn't like same option - // to be defind on both parent-command and sub-command - const { fullObject = false } = args.parent.opts(); + // subcommands inherit flags from their parent; they cannot override them + const { fullObject = false, email = undefined, password = undefined } = args.parent.opts(); const mergedOptions = { ...options, fullObject: fullObject, + email, + password, }; const response = await this.runCreate(encodedJson, mergedOptions); @@ -247,7 +238,7 @@ export class SendProgram extends BaseProgram { writeLn(" You cannot update a File-type Send's file. Just delete and remake it"); writeLn("", true); }) - .action(async (encodedJson: string, options: OptionValues) => { + .action(async (encodedJson: string, options: OptionValues, args: { parent: Command }) => { await this.exitIfLocked(); const getCmd = new SendGetCommand( this.serviceContainer.sendService, @@ -264,7 +255,16 @@ export class SendProgram extends BaseProgram { this.serviceContainer.billingAccountProfileStateService, this.serviceContainer.accountService, ); - const response = await cmd.run(encodedJson, options); + + // subcommands inherit flags from their parent; they cannot override them + const { email = undefined, password = undefined } = args.parent.opts(); + const mergedOptions = { + ...options, + email, + password, + }; + + const response = await cmd.run(encodedJson, mergedOptions); this.processResponse(response); }); } diff --git a/apps/cli/src/tools/send/util.spec.ts b/apps/cli/src/tools/send/util.spec.ts new file mode 100644 index 0000000000..2cfc2a1b4c --- /dev/null +++ b/apps/cli/src/tools/send/util.spec.ts @@ -0,0 +1,194 @@ +import { parseEmail } from "./util"; + +describe("parseEmail", () => { + describe("single email address parsing", () => { + it("should parse a valid single email address", () => { + const result = parseEmail("test@example.com", []); + expect(result).toEqual(["test@example.com"]); + }); + + it("should parse email with dots in local part", () => { + const result = parseEmail("test.user@example.com", []); + expect(result).toEqual(["test.user@example.com"]); + }); + + it("should parse email with underscores and hyphens", () => { + const result = parseEmail("test_user-name@example.com", []); + expect(result).toEqual(["test_user-name@example.com"]); + }); + + it("should parse email with plus sign", () => { + const result = parseEmail("test+user@example.com", []); + expect(result).toEqual(["test+user@example.com"]); + }); + + it("should parse email with dots and hyphens in domain", () => { + const result = parseEmail("user@test-domain.co.uk", []); + expect(result).toEqual(["user@test-domain.co.uk"]); + }); + + it("should add single email to existing previousInput array", () => { + const result = parseEmail("new@example.com", ["existing@test.com"]); + expect(result).toEqual(["existing@test.com", "new@example.com"]); + }); + }); + + describe("comma-separated email lists", () => { + it("should parse comma-separated email list", () => { + const result = parseEmail("test@example.com,user@domain.com", []); + expect(result).toEqual(["test@example.com", "user@domain.com"]); + }); + + it("should parse comma-separated emails with spaces", () => { + const result = parseEmail("test@example.com, user@domain.com, admin@site.org", []); + expect(result).toEqual(["test@example.com", "user@domain.com", "admin@site.org"]); + }); + + it("should combine comma-separated emails with previousInput", () => { + const result = parseEmail("new1@example.com,new2@domain.com", ["existing@test.com"]); + expect(result).toEqual(["existing@test.com", "new1@example.com", "new2@domain.com"]); + }); + + it("should throw error for invalid email in comma-separated list", () => { + expect(() => { + parseEmail("valid@example.com,invalid-email,another@domain.com", []); + }).toThrow("Invalid email address: invalid-email"); + }); + }); + + describe("space-separated email lists", () => { + it("should parse space-separated email list", () => { + const result = parseEmail("test@example.com user@domain.com", []); + expect(result).toEqual(["test@example.com", "user@domain.com"]); + }); + + it("should parse space-separated emails with multiple spaces", () => { + const result = parseEmail("test@example.com user@domain.com admin@site.org", []); + expect(result).toEqual(["test@example.com", "user@domain.com", "admin@site.org"]); + }); + + it("should combine space-separated emails with previousInput", () => { + const result = parseEmail("new1@example.com new2@domain.com", ["existing@test.com"]); + expect(result).toEqual(["existing@test.com", "new1@example.com", "new2@domain.com"]); + }); + + it("should throw error for invalid email in space-separated list", () => { + expect(() => { + parseEmail("valid@example.com invalid-email another@domain.com", []); + }).toThrow("Invalid email address: invalid-email"); + }); + }); + + describe("JSON array input format", () => { + it("should parse valid JSON array of emails", () => { + const result = parseEmail('["test@example.com", "user@domain.com"]', []); + expect(result).toEqual(["test@example.com", "user@domain.com"]); + }); + + it("should parse single email in JSON array", () => { + const result = parseEmail('["test@example.com"]', []); + expect(result).toEqual(["test@example.com"]); + }); + + it("should parse empty JSON array", () => { + const result = parseEmail("[]", []); + expect(result).toEqual([]); + }); + + it("should combine JSON array with previousInput", () => { + const result = parseEmail('["new1@example.com", "new2@domain.com"]', ["existing@test.com"]); + expect(result).toEqual(["existing@test.com", "new1@example.com", "new2@domain.com"]); + }); + + it("should throw error for malformed JSON", () => { + expect(() => { + parseEmail('["test@example.com", "user@domain.com"', []); + }).toThrow(); + }); + + it("should throw error for JSON that is not an array", () => { + expect(() => { + parseEmail('{"email": "test@example.com"}', []); + }).toThrow("Invalid email address:"); + }); + + it("should throw error for JSON string instead of array", () => { + expect(() => { + parseEmail('"test@example.com"', []); + }).toThrow("`input` must be a single address, a comma-separated list, or a JSON array"); + }); + + it("should throw error for JSON number instead of array", () => { + expect(() => { + parseEmail("123", []); + }).toThrow("`input` must be a single address, a comma-separated list, or a JSON array"); + }); + }); + + describe("`previousInput` parameter handling", () => { + it("should handle undefined previousInput", () => { + const result = parseEmail("test@example.com", undefined as any); + expect(result).toEqual(["test@example.com"]); + }); + + it("should handle null previousInput", () => { + const result = parseEmail("test@example.com", null as any); + expect(result).toEqual(["test@example.com"]); + }); + + it("should preserve existing emails in previousInput", () => { + const existing = ["existing1@test.com", "existing2@test.com"]; + const result = parseEmail("new@example.com", existing); + expect(result).toEqual(["existing1@test.com", "existing2@test.com", "new@example.com"]); + }); + }); + + describe("error cases and edge conditions", () => { + it("should throw error for empty string input", () => { + expect(() => { + parseEmail("", []); + }).toThrow("`input` must be a single address, a comma-separated list, or a JSON array"); + }); + + it("should return empty array for whitespace-only input", () => { + const result = parseEmail(" ", []); + expect(result).toEqual([]); + }); + + it("should throw error for invalid single email", () => { + expect(() => { + parseEmail("invalid-email", []); + }).toThrow("`input` must be a single address, a comma-separated list, or a JSON array"); + }); + + it("should throw error for email without @ symbol", () => { + expect(() => { + parseEmail("testexample.com", []); + }).toThrow("`input` must be a single address, a comma-separated list, or a JSON array"); + }); + + it("should throw error for email without domain", () => { + expect(() => { + parseEmail("test@", []); + }).toThrow("`input` must be a single address, a comma-separated list, or a JSON array"); + }); + + it("should throw error for email without local part", () => { + expect(() => { + parseEmail("@example.com", []); + }).toThrow("`input` must be a single address, a comma-separated list, or a JSON array"); + }); + + it("should throw error for input that looks like file path", () => { + expect(() => { + parseEmail("/path/to/file.txt", []); + }).toThrow("`input` must be a single address, a comma-separated list, or a JSON array"); + }); + + it("should throw error for input that looks like URL", () => { + expect(() => { + parseEmail("https://example.com", []); + }).toThrow("`input` must be a single address, a comma-separated list, or a JSON array"); + }); + }); +}); diff --git a/apps/cli/src/tools/send/util.ts b/apps/cli/src/tools/send/util.ts new file mode 100644 index 0000000000..bf66f916bb --- /dev/null +++ b/apps/cli/src/tools/send/util.ts @@ -0,0 +1,55 @@ +/** + * Parses email addresses from various input formats and combines them with previously parsed emails. + * + * Supports: single email, JSON array, comma-separated, or space-separated lists. + * Note: Function signature follows Commander.js option parsing pattern. + * + * @param input - Email input string in any supported format + * @param previousInput - Previously parsed email addresses to append to + * @returns Combined array of email addresses + * @throws {Error} For invalid JSON, non-array JSON, invalid email addresses, or unrecognized format + * + * @example + * parseEmail("user@example.com", []) // ["user@example.com"] + * parseEmail('["user1@example.com", "user2@example.com"]', []) // ["user1@example.com", "user2@example.com"] + * parseEmail("user1@example.com, user2@example.com", []) // ["user1@example.com", "user2@example.com"] + */ +export function parseEmail(input: string, previousInput: string[]) { + let result = previousInput ?? []; + + if (isEmail(input)) { + result.push(input); + } else if (input.startsWith("[")) { + const json = JSON.parse(input); + if (!Array.isArray(json)) { + throw new Error("invalid JSON"); + } + + result = result.concat(json); + } else if (input.includes(",")) { + result = result.concat(parseList(input, ",")); + } else if (input.includes(" ")) { + result = result.concat(parseList(input, " ")); + } else { + throw new Error("`input` must be a single address, a comma-separated list, or a JSON array"); + } + + return result; +} + +function isEmail(input: string) { + return !!input && !!input.match(/^([\w._+-]+?)@([\w._+-]+?)$/); +} + +function parseList(value: string, separator: string) { + const parts = value + .split(separator) + .map((v) => v.trim()) + .filter((v) => !!v.length); + const invalid = parts.find((v) => !isEmail(v)); + if (invalid) { + throw new Error(`Invalid email address: ${invalid}`); + } + + return parts; +} diff --git a/libs/common/src/tools/send/models/data/send.data.ts b/libs/common/src/tools/send/models/data/send.data.ts index e4df5e48dc..2c6377de0c 100644 --- a/libs/common/src/tools/send/models/data/send.data.ts +++ b/libs/common/src/tools/send/models/data/send.data.ts @@ -21,6 +21,7 @@ export class SendData { expirationDate: string; deletionDate: string; password: string; + emails: string; disabled: boolean; hideEmail: boolean; @@ -41,6 +42,7 @@ export class SendData { this.expirationDate = response.expirationDate; this.deletionDate = response.deletionDate; this.password = response.password; + this.emails = response.emails; this.disabled = response.disable; this.hideEmail = response.hideEmail; diff --git a/libs/common/src/tools/send/models/domain/send.spec.ts b/libs/common/src/tools/send/models/domain/send.spec.ts index 8df9a14410..e9b0ae7b3b 100644 --- a/libs/common/src/tools/send/models/domain/send.spec.ts +++ b/libs/common/src/tools/send/models/domain/send.spec.ts @@ -29,14 +29,15 @@ describe("Send", () => { text: "encText", hidden: true, }, - file: null, + file: null!, key: "encKey", - maxAccessCount: null, + maxAccessCount: null!, accessCount: 10, revisionDate: "2022-01-31T12:00:00.000Z", expirationDate: "2022-01-31T12:00:00.000Z", deletionDate: "2022-01-31T12:00:00.000Z", password: "password", + emails: null!, disabled: false, hideEmail: true, }; @@ -86,6 +87,7 @@ describe("Send", () => { expirationDate: new Date("2022-01-31T12:00:00.000Z"), deletionDate: new Date("2022-01-31T12:00:00.000Z"), password: "password", + emails: null!, disabled: false, hideEmail: true, }); diff --git a/libs/common/src/tools/send/models/domain/send.ts b/libs/common/src/tools/send/models/domain/send.ts index 89fe92c2c7..48057aedd2 100644 --- a/libs/common/src/tools/send/models/domain/send.ts +++ b/libs/common/src/tools/send/models/domain/send.ts @@ -27,6 +27,7 @@ export class Send extends Domain { expirationDate: Date; deletionDate: Date; password: string; + emails: string; disabled: boolean; hideEmail: boolean; @@ -53,6 +54,7 @@ export class Send extends Domain { this.maxAccessCount = obj.maxAccessCount; this.accessCount = obj.accessCount; this.password = obj.password; + this.emails = obj.emails; this.disabled = obj.disabled; this.revisionDate = obj.revisionDate != null ? new Date(obj.revisionDate) : null; this.deletionDate = obj.deletionDate != null ? new Date(obj.deletionDate) : null; diff --git a/libs/common/src/tools/send/models/request/send.request.ts b/libs/common/src/tools/send/models/request/send.request.ts index 9e4f1e1483..f7e3ff26d7 100644 --- a/libs/common/src/tools/send/models/request/send.request.ts +++ b/libs/common/src/tools/send/models/request/send.request.ts @@ -17,6 +17,7 @@ export class SendRequest { text: SendTextApi; file: SendFileApi; password: string; + emails: string; disabled: boolean; hideEmail: boolean; @@ -30,6 +31,7 @@ export class SendRequest { this.deletionDate = send.deletionDate != null ? send.deletionDate.toISOString() : null; this.key = send.key != null ? send.key.encryptedString : null; this.password = send.password; + this.emails = send.emails; this.disabled = send.disabled; this.hideEmail = send.hideEmail; diff --git a/libs/common/src/tools/send/models/response/send.response.ts b/libs/common/src/tools/send/models/response/send.response.ts index 76550f5cdf..5c6bd4dc1a 100644 --- a/libs/common/src/tools/send/models/response/send.response.ts +++ b/libs/common/src/tools/send/models/response/send.response.ts @@ -20,6 +20,7 @@ export class SendResponse extends BaseResponse { expirationDate: string; deletionDate: string; password: string; + emails: string; disable: boolean; hideEmail: boolean; @@ -37,6 +38,7 @@ export class SendResponse extends BaseResponse { this.expirationDate = this.getResponseProperty("ExpirationDate"); this.deletionDate = this.getResponseProperty("DeletionDate"); this.password = this.getResponseProperty("Password"); + this.emails = this.getResponseProperty("Emails"); this.disable = this.getResponseProperty("Disabled") || false; this.hideEmail = this.getResponseProperty("HideEmail") || false; diff --git a/libs/common/src/tools/send/models/view/send.view.ts b/libs/common/src/tools/send/models/view/send.view.ts index 2c269892a6..54657b1243 100644 --- a/libs/common/src/tools/send/models/view/send.view.ts +++ b/libs/common/src/tools/send/models/view/send.view.ts @@ -26,6 +26,7 @@ export class SendView implements View { deletionDate: Date = null; expirationDate: Date = null; password: string = null; + emails: string[] = []; disabled = false; hideEmail = false; diff --git a/libs/common/src/tools/send/services/send.service.ts b/libs/common/src/tools/send/services/send.service.ts index 57463b3b42..6e2b4391c9 100644 --- a/libs/common/src/tools/send/services/send.service.ts +++ b/libs/common/src/tools/send/services/send.service.ts @@ -74,7 +74,12 @@ export class SendService implements InternalSendServiceAbstraction { model.key = key.material; model.cryptoKey = key.derivedKey; } - if (password != null) { + + const hasEmails = (model.emails?.length ?? 0) > 0; + if (hasEmails) { + send.emails = model.emails.join(","); + send.password = null; + } else if (password != null) { // Note: Despite being called key, the passwordKey is not used for encryption. // It is used as a static proof that the client knows the password, and has the encryption key. const passwordKey = await this.keyGenerationService.deriveKeyFromPassword(