diff --git a/libs/importer/spec/passwordxp-csv-importer.spec.ts b/libs/importer/spec/passwordxp-csv-importer.spec.ts
new file mode 100644
index 0000000000..f707b1138c
--- /dev/null
+++ b/libs/importer/spec/passwordxp-csv-importer.spec.ts
@@ -0,0 +1,148 @@
+import { CipherType } from "@bitwarden/common/vault/enums";
+
+import { PasswordXPCsvImporter } from "../src/importers";
+import { ImportResult } from "../src/models/import-result";
+
+import { noFolder } from "./test-data/passwordxp-csv/no-folder.csv";
+import { withFolders } from "./test-data/passwordxp-csv/passwordxp-with-folders.csv";
+import { withoutFolders } from "./test-data/passwordxp-csv/passwordxp-without-folders.csv";
+
+describe("PasswordXPCsvImporter", () => {
+ let importer: PasswordXPCsvImporter;
+
+ beforeEach(() => {
+ importer = new PasswordXPCsvImporter();
+ });
+
+ it("should return success false if CSV data is null", async () => {
+ const data = "";
+ const result: ImportResult = await importer.parse(data);
+ expect(result.success).toBe(false);
+ });
+
+ it("should skip rows starting with >>>", async () => {
+ const result: ImportResult = await importer.parse(noFolder);
+ expect(result.success).toBe(true);
+ expect(result.ciphers.length).toBe(0);
+ });
+
+ it("should parse CSV data and return success true", async () => {
+ const result: ImportResult = await importer.parse(withoutFolders);
+ expect(result.success).toBe(true);
+ expect(result.ciphers.length).toBe(4);
+
+ let cipher = result.ciphers.shift();
+ expect(cipher.type).toBe(CipherType.Login);
+ expect(cipher.name).toBe("Title2");
+ expect(cipher.notes).toBe("Test Notes");
+ expect(cipher.login.username).toBe("Username2");
+ expect(cipher.login.password).toBe("12345678");
+ expect(cipher.login.uris[0].uri).toBe("http://URL2.com");
+
+ cipher = result.ciphers.shift();
+ expect(cipher.type).toBe(CipherType.Login);
+ expect(cipher.name).toBe("Title Test 1");
+ expect(cipher.notes).toBe("Test Notes 2");
+ expect(cipher.login.username).toBe("Username1");
+ expect(cipher.login.password).toBe("Password1");
+ expect(cipher.login.uris[0].uri).toBe("http://URL1.com");
+
+ cipher = result.ciphers.shift();
+ expect(cipher.type).toBe(CipherType.SecureNote);
+ expect(cipher.name).toBe("Certificate 1");
+ expect(cipher.notes).toBe("Test Notes Certicate 1");
+
+ cipher = result.ciphers.shift();
+ expect(cipher.type).toBe(CipherType.Login);
+ expect(cipher.name).toBe("test");
+ expect(cipher.notes).toBe("Test Notes 3");
+ expect(cipher.login.username).toBe("testtest");
+ expect(cipher.login.password).toBe("test");
+ expect(cipher.login.uris[0].uri).toBe("http://test");
+ });
+
+ it("should parse CSV data and import unmapped columns as custom fields", async () => {
+ const result: ImportResult = await importer.parse(withoutFolders);
+ expect(result.success).toBe(true);
+
+ const cipher = result.ciphers.shift();
+ expect(cipher.type).toBe(CipherType.Login);
+ expect(cipher.name).toBe("Title2");
+ expect(cipher.notes).toBe("Test Notes");
+ expect(cipher.login.username).toBe("Username2");
+ expect(cipher.login.password).toBe("12345678");
+ expect(cipher.login.uris[0].uri).toBe("http://URL2.com");
+
+ expect(cipher.fields.length).toBe(5);
+ let field = cipher.fields.shift();
+ expect(field.name).toBe("Account");
+ expect(field.value).toBe("Account2");
+
+ field = cipher.fields.shift();
+ expect(field.name).toBe("Modified");
+ expect(field.value).toBe("27-3-2024 08:11:21");
+
+ field = cipher.fields.shift();
+ expect(field.name).toBe("Created");
+ expect(field.value).toBe("27-3-2024 08:11:21");
+
+ field = cipher.fields.shift();
+ expect(field.name).toBe("Expire on");
+ expect(field.value).toBe("27-5-2024 08:11:21");
+
+ field = cipher.fields.shift();
+ expect(field.name).toBe("Modified by");
+ expect(field.value).toBe("someone");
+ });
+
+ it("should parse CSV data with folders and assign items to them", async () => {
+ const result: ImportResult = await importer.parse(withFolders);
+ expect(result.success).toBe(true);
+ expect(result.ciphers.length).toBe(5);
+
+ expect(result.folders.length).toBe(3);
+ let folder = result.folders.shift();
+ expect(folder.name).toEqual("Test Folder");
+ folder = result.folders.shift();
+ expect(folder.name).toEqual("Cert folder");
+ folder = result.folders.shift();
+ expect(folder.name).toEqual("Cert folder/Nested folder");
+
+ expect(result.folderRelationships.length).toBe(4);
+ let folderRelationship = result.folderRelationships.shift();
+ expect(folderRelationship).toEqual([1, 0]);
+ folderRelationship = result.folderRelationships.shift();
+ expect(folderRelationship).toEqual([2, 1]);
+ folderRelationship = result.folderRelationships.shift();
+ expect(folderRelationship).toEqual([3, 1]);
+ folderRelationship = result.folderRelationships.shift();
+ expect(folderRelationship).toEqual([4, 2]);
+ folderRelationship = result.folderRelationships.shift();
+ });
+
+ it("should convert folders to collections when importing into an organization", async () => {
+ importer.organizationId = "someOrg";
+ const result: ImportResult = await importer.parse(withFolders);
+ expect(result.success).toBe(true);
+ expect(result.ciphers.length).toBe(5);
+
+ expect(result.collections.length).toBe(3);
+ expect(result.collections[0].name).toEqual("Test Folder");
+ expect(result.collectionRelationships[0]).toEqual([1, 0]);
+ expect(result.collections[1].name).toEqual("Cert folder");
+ expect(result.collectionRelationships[1]).toEqual([2, 1]);
+ expect(result.collectionRelationships[2]).toEqual([3, 1]);
+ expect(result.collections[2].name).toEqual("Cert folder/Nested folder");
+
+ expect(result.collectionRelationships.length).toBe(4);
+ let collectionRelationship = result.collectionRelationships.shift();
+ expect(collectionRelationship).toEqual([1, 0]);
+ collectionRelationship = result.collectionRelationships.shift();
+ expect(collectionRelationship).toEqual([2, 1]);
+ collectionRelationship = result.collectionRelationships.shift();
+ expect(collectionRelationship).toEqual([3, 1]);
+ collectionRelationship = result.collectionRelationships.shift();
+ expect(collectionRelationship).toEqual([4, 2]);
+ collectionRelationship = result.collectionRelationships.shift();
+ });
+});
diff --git a/libs/importer/spec/test-data/passwordxp-csv/no-folder.csv.ts b/libs/importer/spec/test-data/passwordxp-csv/no-folder.csv.ts
new file mode 100644
index 0000000000..056db68d5a
--- /dev/null
+++ b/libs/importer/spec/test-data/passwordxp-csv/no-folder.csv.ts
@@ -0,0 +1,2 @@
+export const noFolder = `Title;User name;Account;URL;Password;Modified;Created;Expire on;Description;Modified by
+>>>`;
diff --git a/libs/importer/spec/test-data/passwordxp-csv/passwordxp-with-folders.csv.ts b/libs/importer/spec/test-data/passwordxp-csv/passwordxp-with-folders.csv.ts
new file mode 100644
index 0000000000..c7cfe82575
--- /dev/null
+++ b/libs/importer/spec/test-data/passwordxp-csv/passwordxp-with-folders.csv.ts
@@ -0,0 +1,13 @@
+export const withFolders = `Title;User name;Account;URL;Password;Modified;Created;Expire on;Description;Modified by
+>>>
+Title2;Username2;Account2;http://URL2.com;12345678;27-3-2024 08:11:21;27-3-2024 08:11:21;;;
+
+[Test Folder]
+Title Test 1;Username1;Account1;http://URL1.com;Password1;27-3-2024 08:10:52;27-3-2024 08:10:52;;;
+
+[Cert folder]
+Certificate 1;;;;;27-3-2024 10:22:39;27-3-2024 10:22:39;;;
+test;testtest;;http://test;test;27-3-2024 12:36:59;27-3-2024 12:36:59;;;
+
+[Cert folder\\Nested folder];
+test2;testtest;;http://test;test;27-3-2024 12:36:59;27-3-2024 12:36:59;;;`;
diff --git a/libs/importer/spec/test-data/passwordxp-csv/passwordxp-without-folders.csv.ts b/libs/importer/spec/test-data/passwordxp-csv/passwordxp-without-folders.csv.ts
new file mode 100644
index 0000000000..eb9055611b
--- /dev/null
+++ b/libs/importer/spec/test-data/passwordxp-csv/passwordxp-without-folders.csv.ts
@@ -0,0 +1,7 @@
+export const withoutFolders = `Title;User name;Account;URL;Password;Modified;Created;Expire on;Description;Modified by
+>>>
+Title2;Username2;Account2;http://URL2.com;12345678;27-3-2024 08:11:21;27-3-2024 08:11:21;27-5-2024 08:11:21;Test Notes;someone
+Title Test 1;Username1;Account1;http://URL1.com;Password1;27-3-2024 08:10:52;27-3-2024 08:10:52;;Test Notes 2;
+Certificate 1;;;;;27-3-2024 10:22:39;27-3-2024 10:22:39;;Test Notes Certicate 1;
+test;testtest;;http://test;test;27-3-2024 12:36:59;27-3-2024 12:36:59;;Test Notes 3;
+`;
diff --git a/libs/importer/src/components/import.component.html b/libs/importer/src/components/import.component.html
index 5b67fc47a7..33056265de 100644
--- a/libs/importer/src/components/import.component.html
+++ b/libs/importer/src/components/import.component.html
@@ -380,6 +380,11 @@
In the ProtonPass browser extension, go to Settings > Export. Export without PGP
encryption and save the zip file.
+
+ Select Database → Export to file... menu. → Within the export options, ensure
+ Folder names and Column titles is checked. → Press Browse... button and choose the
+ target file and set it's type to CSV file.
+
Open the FullClient, go to the Main Menu and select Export. Start the export passwords
wizard and follow the instructions to export a CSV file.
diff --git a/libs/importer/src/importers/index.ts b/libs/importer/src/importers/index.ts
index 45658f1e6c..19b22cfa80 100644
--- a/libs/importer/src/importers/index.ts
+++ b/libs/importer/src/importers/index.ts
@@ -45,6 +45,7 @@ export { PasswordBossJsonImporter } from "./passwordboss-json-importer";
export { PasswordDragonXmlImporter } from "./passworddragon-xml-importer";
export { PasswordSafeXmlImporter } from "./passwordsafe-xml-importer";
export { PasswordWalletTxtImporter } from "./passwordwallet-txt-importer";
+export { PasswordXPCsvImporter } from "./passwordxp-csv-importer";
export { ProtonPassJsonImporter } from "./protonpass/protonpass-json-importer";
export { PsonoJsonImporter } from "./psono/psono-json-importer";
export { RememBearCsvImporter } from "./remembear-csv-importer";
diff --git a/libs/importer/src/importers/passwordxp-csv-importer.ts b/libs/importer/src/importers/passwordxp-csv-importer.ts
new file mode 100644
index 0000000000..461432e98d
--- /dev/null
+++ b/libs/importer/src/importers/passwordxp-csv-importer.ts
@@ -0,0 +1,78 @@
+import { Utils } from "@bitwarden/common/platform/misc/utils";
+import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
+
+import { ImportResult } from "../models/import-result";
+
+import { BaseImporter } from "./base-importer";
+import { Importer } from "./importer";
+
+const _mappedColumns = new Set(["Title", "Username", "URL", "Password", "Description"]);
+
+/**
+ * PasswordXP CSV importer
+ */
+export class PasswordXPCsvImporter extends BaseImporter implements Importer {
+ /**
+ * Parses the PasswordXP CSV data.
+ * @param data
+ */
+ parse(data: string): Promise {
+ // The header column 'User name' is parsed by the parser, but cannot be used as a variable. This converts it to a valid variable name, prior to parsing.
+ data = data.replace(";User name;", ";Username;");
+
+ const result = new ImportResult();
+ const results = this.parseCsv(data, true, { skipEmptyLines: true });
+ if (results == null) {
+ result.success = false;
+ return Promise.resolve(result);
+ }
+ let currentFolderName = "";
+ results.forEach((row) => {
+ // Skip rows starting with '>>>' as they indicate items following have no folder assigned to them
+ if (row.Title == ">>>") {
+ return;
+ }
+
+ const title = row.Title;
+ // If the title is in the format [title], then it is a folder name
+ if (title.startsWith("[") && title.endsWith("]")) {
+ currentFolderName = title.startsWith("/")
+ ? title.replace("/", "")
+ : title.substring(1, title.length - 1);
+ return;
+ }
+
+ if (!Utils.isNullOrWhitespace(currentFolderName)) {
+ this.processFolder(result, currentFolderName);
+ }
+
+ const cipher = this.initLoginCipher();
+ cipher.name = this.getValueOrDefault(row.Title);
+ cipher.login.username = this.getValueOrDefault(row.Username);
+ cipher.notes = this.getValueOrDefault(row.Description);
+ cipher.login.uris = this.makeUriArray(row.URL);
+ cipher.login.password = this.getValueOrDefault(row.Password);
+
+ this.importUnmappedFields(cipher, row, _mappedColumns);
+
+ this.convertToNoteIfNeeded(cipher);
+ this.cleanupCipher(cipher);
+ result.ciphers.push(cipher);
+ });
+
+ if (this.organization) {
+ this.moveFoldersToCollections(result);
+ }
+
+ result.success = true;
+ return Promise.resolve(result);
+ }
+
+ private importUnmappedFields(cipher: CipherView, row: any, mappedValues: Set) {
+ const unmappedFields = Object.keys(row).filter((x) => !mappedValues.has(x));
+ unmappedFields.forEach((key) => {
+ const item = row as any;
+ this.processKvp(cipher, key, item[key]);
+ });
+ }
+}
diff --git a/libs/importer/src/models/import-options.ts b/libs/importer/src/models/import-options.ts
index f656c728ff..bd3202067c 100644
--- a/libs/importer/src/models/import-options.ts
+++ b/libs/importer/src/models/import-options.ts
@@ -70,6 +70,7 @@ export const regularImportOptions = [
{ id: "nordpasscsv", name: "Nordpass (csv)" },
{ id: "psonojson", name: "Psono (json)" },
{ id: "passkyjson", name: "Passky (json)" },
+ { id: "passwordxpcsv", name: "Password XP (csv)" },
{ id: "netwrixpasswordsecure", name: "Netwrix Password Secure (csv)" },
] as const;
diff --git a/libs/importer/src/services/import.service.ts b/libs/importer/src/services/import.service.ts
index 6bfc5d5ce9..fcbbc2c06e 100644
--- a/libs/importer/src/services/import.service.ts
+++ b/libs/importer/src/services/import.service.ts
@@ -84,6 +84,7 @@ import {
UpmCsvImporter,
YotiCsvImporter,
ZohoVaultCsvImporter,
+ PasswordXPCsvImporter,
} from "../importers";
import { Importer } from "../importers/importer";
import {
@@ -336,6 +337,8 @@ export class ImportService implements ImportServiceAbstraction {
return new PasskyJsonImporter();
case "protonpass":
return new ProtonPassJsonImporter(this.i18nService);
+ case "passwordxpcsv":
+ return new PasswordXPCsvImporter();
case "netwrixpasswordsecure":
return new NetwrixPasswordSecureCsvImporter();
default: