mirror of
https://github.com/bitwarden/browser
synced 2025-12-10 13:23:34 +00:00
[PM-19054] configure send with email otp authentication via cli (#15360)
This commit is contained in:
@@ -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.");
|
||||
|
||||
@@ -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<string>;
|
||||
hidden: boolean;
|
||||
|
||||
constructor(passedOptions: Record<string, any>) {
|
||||
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;
|
||||
|
||||
@@ -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<string, any>) {
|
||||
this.itemId = passedOptions?.itemId || passedOptions?.itemid;
|
||||
this.password = passedOptions.password;
|
||||
this.emails = passedOptions.email;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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";
|
||||
|
||||
35
apps/cli/src/tools/send/commands/template.command.ts
Normal file
35
apps/cli/src/tools/send/commands/template.command.ts
Normal file
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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<string>;
|
||||
disabled: boolean;
|
||||
hideEmail: boolean;
|
||||
|
||||
|
||||
@@ -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 <password>",
|
||||
"optional password to access this Send. Can also be specified in JSON.",
|
||||
).conflicts("email"),
|
||||
)
|
||||
.option(
|
||||
"--email <email>",
|
||||
"optional emails to access this Send. Can also be specified in JSON.",
|
||||
parseEmail,
|
||||
)
|
||||
.option("-a, --maxAccessCount <amount>", "The amount of max possible accesses.")
|
||||
.option("--hidden", "Hide <data> 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("<object>", "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,
|
||||
.action((options: OptionValues) =>
|
||||
this.processResponse(new SendTemplateCommand().run(options.object)),
|
||||
);
|
||||
const response = await cmd.run("template", object, null);
|
||||
this.processResponse(response);
|
||||
});
|
||||
}
|
||||
|
||||
private getCommand(): Command {
|
||||
@@ -208,10 +203,6 @@ export class SendProgram extends BaseProgram {
|
||||
.option("--file <path>", "file to Send. Can also be specified in parent's JSON.")
|
||||
.option("--text <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 <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);
|
||||
});
|
||||
}
|
||||
|
||||
194
apps/cli/src/tools/send/util.spec.ts
Normal file
194
apps/cli/src/tools/send/util.spec.ts
Normal file
@@ -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");
|
||||
});
|
||||
});
|
||||
});
|
||||
55
apps/cli/src/tools/send/util.ts
Normal file
55
apps/cli/src/tools/send/util.ts
Normal file
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -26,6 +26,7 @@ export class SendView implements View {
|
||||
deletionDate: Date = null;
|
||||
expirationDate: Date = null;
|
||||
password: string = null;
|
||||
emails: string[] = [];
|
||||
disabled = false;
|
||||
hideEmail = false;
|
||||
|
||||
|
||||
@@ -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(
|
||||
|
||||
Reference in New Issue
Block a user