From 02549c16a04dfdb194ed11f91fab2339b8b7fddc Mon Sep 17 00:00:00 2001 From: tamarafinogina Date: Tue, 31 Mar 2026 14:49:02 +0200 Subject: [PATCH] remove email search --- package.json | 2 - src/email-search/index.ts | 3 - src/email-search/indexedDB.ts | 351 -------------------------- src/email-search/mailCache.ts | 152 ----------- src/email-search/search.ts | 207 --------------- src/email-search/utils.ts | 17 -- src/index.ts | 25 -- src/types.ts | 33 --- tests/email-search/cacheLimit.test.ts | 29 --- tests/email-search/helper.ts | 55 ---- tests/email-search/indexedDB.test.ts | 163 ------------ tests/email-search/mailCache.test.ts | 141 ----------- tests/email-search/search.test.ts | 73 ------ tsdown.config.ts | 1 - yarn.lock | 10 - 15 files changed, 1262 deletions(-) delete mode 100644 src/email-search/index.ts delete mode 100644 src/email-search/indexedDB.ts delete mode 100644 src/email-search/mailCache.ts delete mode 100644 src/email-search/search.ts delete mode 100644 src/email-search/utils.ts delete mode 100644 tests/email-search/cacheLimit.test.ts delete mode 100644 tests/email-search/helper.ts delete mode 100644 tests/email-search/indexedDB.test.ts delete mode 100644 tests/email-search/mailCache.test.ts delete mode 100644 tests/email-search/search.test.ts diff --git a/package.json b/package.json index 312fc35..b27537c 100644 --- a/package.json +++ b/package.json @@ -41,10 +41,8 @@ "@noble/hashes": "^2.0.1", "@noble/post-quantum": "^0.5.2", "@scure/bip39": "^2.0.1", - "flexsearch": "^0.8.205", "hash-wasm": "^4.12.0", "husky": "^9.1.7", - "idb": "^8.0.3", "uuid": "^13.0.0" }, "exports": { diff --git a/src/email-search/index.ts b/src/email-search/index.ts deleted file mode 100644 index f0bd3d0..0000000 --- a/src/email-search/index.ts +++ /dev/null @@ -1,3 +0,0 @@ -export * from './indexedDB'; -export * from './mailCache'; -export * from './search'; diff --git a/src/email-search/indexedDB.ts b/src/email-search/indexedDB.ts deleted file mode 100644 index 93b447e..0000000 --- a/src/email-search/indexedDB.ts +++ /dev/null @@ -1,351 +0,0 @@ -import { DBSchema, openDB, deleteDB, IDBPDatabase } from 'idb'; -import { StoredEmail, Email } from '../types'; -import { decryptEmailBody, encryptEmailBodyWithKey } from '../email-crypto/core'; -import { deriveSymmetricKeyFromContext } from '../derive-key'; -import { CONTEXT_INDEX, DB_LABEL, DB_VERSION } from '../constants'; - -export type MailDB = IDBPDatabase; - -export interface EncryptedSearchDB extends DBSchema { - email: { - key: string; - value: StoredEmail; - indexes: { byTime: number[] }; - }; -} - -/** - * Returns IndexedDB database name for the given user - * - * @param userID - The user ID - * @returns The database name - */ -const getDatabaseName = (userID: string): string => { - return `ES:${userID}:DB`; -}; - -/** - * Opens IndexedDB database for the given user - * - * @param userID - The user ID - * @returns The database - */ -export const openDatabase = async (userID: string): Promise => { - try { - const dbName = getDatabaseName(userID); - return openDB(dbName, DB_VERSION, { - upgrade(db) { - if (!db.objectStoreNames.contains(DB_LABEL)) { - const store = db.createObjectStore(DB_LABEL, { keyPath: 'id' }); - store.createIndex('byTime', 'params.createdAt'); - } - }, - }); - } catch (error) { - throw new Error(`Cannot open a database for the user ${userID}`, { cause: error }); - } -}; - -/** - * Closes the IndexedDB database - * - * @param esDB - The database - */ -export const closeDatabase = (esDB: MailDB): void => { - return esDB.close(); -}; - -/** - * Deletes IndexedDB database for the given user - * - * @param userID - The user ID - * @returns The database - */ -export const deleteDatabase = async (userID: string): Promise => { - const dbName = getDatabaseName(userID); - return deleteDB(dbName); -}; - -/** - * Derives database encryption key for the given user - * - * @param userID - The user ID - * @returns The symmetric key for protecting database - */ -export const deriveIndexKey = async (baseKey: Uint8Array): Promise => { - return deriveSymmetricKeyFromContext(CONTEXT_INDEX, baseKey); -}; - -/** - * Encrypts the given email and stores it in the IndexedDB database - * - * @param newEmailToStore - The email for storing - * @param indexKey - The symmetric key for protecting database - * @param esDB - The database - */ -export const encryptAndStoreEmail = async ( - newEmailToStore: Email, - indexKey: Uint8Array, - esDB: MailDB, -): Promise => { - try { - const enc = await encryptEmailBodyWithKey(newEmailToStore.body, indexKey); - const encryptedEmail: StoredEmail = { encEmailBody: enc, params: newEmailToStore.params, id: newEmailToStore.id }; - await esDB.put(DB_LABEL, encryptedEmail); - } catch (error) { - throw new Error('Cannot encrypt and add the given email to the database', { cause: error }); - } -}; - -/** - * Encrypts the given set of emails and stores it in the IndexedDB database - * - * @param newEmailsToStore - The set of emails for storing - * @param indexKey - The symmetric key key for protecting database - * @param esDB - The database - */ -export const encryptAndStoreManyEmail = async ( - newEmailsToStore: Email[], - indexKey: Uint8Array, - esDB: MailDB, -): Promise => { - try { - const encryptedEmails = await Promise.all( - newEmailsToStore.map(async (email: Email) => { - const encEmailBody = await encryptEmailBodyWithKey(email.body, indexKey); - - return { encEmailBody, params: email.params, id: email.id }; - }), - ); - - const tr = esDB.transaction(DB_LABEL, 'readwrite'); - await Promise.all([...encryptedEmails.map((encEmail) => tr.store.put(encEmail)), tr.done]); - } catch (error) { - throw new Error('Cannot encrypt and add emails to the database', { cause: error }); - } -}; - -/** - * Decrypts the given email - * - * @param indexKey - The symmetric key key for protecting database - * @param encryptedEmail - The encrypted email - * @returns The decrypted email - */ -const decryptEmail = async (indexKey: Uint8Array, encryptedEmail: StoredEmail): Promise => { - try { - const email = await decryptEmailBody(encryptedEmail.encEmailBody, indexKey); - return { body: email, params: encryptedEmail.params, id: encryptedEmail.id }; - } catch (error) { - throw new Error('Cannot decrypt the given email', { cause: error }); - } -}; - -/** - * Fetches the email from the database and decrypts it - * - * @param emailID - The email identifier - * @param indexKey - The symmetric key key for protecting database - * @param encryptedEmail - The encrypted email - * @returns The decrypted email - */ -export const getAndDecryptEmail = async (emailID: string, indexKey: Uint8Array, esDB: MailDB): Promise => { - try { - const encryptedEmail = await esDB.get(DB_LABEL, emailID); - if (!encryptedEmail) { - throw new Error(`DB cannot find email with id ${emailID}`); - } - return decryptEmail(indexKey, encryptedEmail); - } catch (error) { - throw new Error(`Cannot fetch the email ${emailID} from the database`, { cause: error }); - } -}; - -/** - * Fetches all email from the database and decrypts them - * - * @param indexKey - The symmetric key key for protecting database - * @param esDB - The database - * @returns The decrypted emails - */ -export const getAndDecryptAllEmails = async (indexKey: Uint8Array, esDB: MailDB): Promise => { - try { - const encryptedEmails = await esDB.getAll(DB_LABEL); - - const decryptedEmails = await Promise.all( - encryptedEmails.map(async (encEmail) => { - const body = await decryptEmailBody(encEmail.encEmailBody, indexKey); - return { body, params: encEmail.params, id: encEmail.id }; - }), - ); - - return decryptedEmails.filter((email): email is Email => email !== null); - } catch (error) { - throw new Error('Cannot fetch and decrypt all emails from the database', { cause: error }); - } -}; - -/** - * Deletes the email from the database - * - * @param emailID - The email identifier - * @param esDB - The database - */ -export const deleteEmail = async (emailID: string, esDB: MailDB): Promise => { - await esDB.delete(DB_LABEL, emailID); -}; - -/** - * Returns the number of stored email - * - * @param esDB - The database - * @returns The number of stored emails - */ -export const getEmailCount = async (esDB: MailDB): Promise => { - return await esDB.count(DB_LABEL); -}; - -/** - * Removes the given number of oldests emails from the database - * - * @param emailsToDelete - The number of emails to delete - * @param esDB - The database - */ -export const deleteOldestEmails = async (emailsToDelete: number, esDB: MailDB): Promise => { - try { - const tx = esDB.transaction(DB_LABEL, 'readwrite'); - const index = tx.store.index('byTime'); - - let cursor = await index.openCursor(); - let deletedCount = 0; - - while (cursor && deletedCount < emailsToDelete) { - await cursor.delete(); - deletedCount++; - cursor = await cursor.continue(); - } - - await tx.done; - } catch (error) { - throw new Error(`Cannot delete ${emailsToDelete} oldests emails from the database`, { cause: error }); - } -}; - -/** - * Enforces the maximum email number in the database - * - * @param esDB - The database - * @param max - The maximum allowed number of emails - */ -export const enforceMaxEmailNumber = async (esDB: MailDB, max: number): Promise => { - try { - const currentCount = await getEmailCount(esDB); - if (currentCount <= max) { - return; - } - await deleteOldestEmails(currentCount - max, esDB); - } catch (error) { - throw new Error(`Cannot enforce the maximum of ${max} emails on the database`, { cause: error }); - } -}; - -/** - * Fetches all emails from the database, decrypts them and sortes the results in the specified order - * - * @param esDB - The database - * @param indexKey - The symmetric key key for protecting database - * @param direction - The order of sorting. If 'next' - oldest first, if 'prev' - newest first - * @returns Decryped emails in the specified order - */ -const fetchEmails = async (esDB: MailDB, indexKey: Uint8Array, direction: 'next' | 'prev'): Promise => { - try { - const tx = esDB.transaction(DB_LABEL, 'readonly'); - const index = tx.store.index('byTime'); - - const encryptedEmails: StoredEmail[] = []; - let cursor = await index.openCursor(null, direction); - - while (cursor) { - encryptedEmails.push(cursor.value); - cursor = await cursor.continue(); - } - - const emails = await Promise.all(encryptedEmails.map((encryptedEmail) => decryptEmail(indexKey, encryptedEmail))); - - return emails; - } catch (error) { - throw new Error('Cannot fetch emails from database', { cause: error }); - } -}; - -/** - * Fetches all emails from the database, decrypts them and sortes the results based on the creation time (newest first) - * - * @param esDB - The database - * @param indexKey - The symmetric Uint8Array key for protecting database - * @returns The number of stored emails - */ -export const getAllEmailsSortedNewestFirst = async (esDB: MailDB, indexKey: Uint8Array): Promise => { - return fetchEmails(esDB, indexKey, 'prev'); -}; - -/** - * Fetches all emails from the database, decrypts them and sortes the results based on the creation time (oldest first) - * - * @param esDB - The database - * @param indexKey - The symmetric key for protecting database - * @returns The number of stored emails - */ -export const getAllEmailsSortedOldestFirst = async (esDB: MailDB, indexKey: Uint8Array): Promise => { - return fetchEmails(esDB, indexKey, 'next'); -}; - -/** - * Fetches a batch of emails from the database, decrypts them and sortes the results based on the creation time (newest first) - * - * @param esDB - The database - * @param indexKey - The symmetric key for protecting database - * @param batchSize - The size of the batch - * @param startCursor - The starting point (optional). If not given, starts from the beginning - * @returns The number of stored emails - */ -export const getEmailBatch = async ( - esDB: MailDB, - indexKey: Uint8Array, - batchSize: number, - startCursor?: IDBValidKey, -): Promise<{ emails: Email[]; nextCursor?: IDBValidKey }> => { - try { - const tx = esDB.transaction(DB_LABEL, 'readonly'); - const index = tx.store.index('byTime'); - - const encryptedEmails: StoredEmail[] = []; - let cursor; - - if (startCursor) { - const range = IDBKeyRange.upperBound(startCursor, true); - cursor = await index.openCursor(range, 'prev'); - } else { - cursor = await index.openCursor(null, 'prev'); - } - - let count = 0; - let nextCursor: IDBValidKey | undefined; - - while (cursor && count < batchSize) { - encryptedEmails.push(cursor.value); - nextCursor = cursor.key; - count++; - cursor = await cursor.continue(); - } - - const emails = await Promise.all(encryptedEmails.map((encryptedEmail) => decryptEmail(indexKey, encryptedEmail))); - - return { - emails, - nextCursor: count === batchSize ? nextCursor : undefined, - }; - } catch (error) { - throw new Error(`Cannot fetch email batch of ${batchSize} from the database`, { cause: error }); - } -}; diff --git a/src/email-search/mailCache.ts b/src/email-search/mailCache.ts deleted file mode 100644 index c197231..0000000 --- a/src/email-search/mailCache.ts +++ /dev/null @@ -1,152 +0,0 @@ -import { MAX_CACHE_SIZE, MAX_EMAIL_PER_BATCH } from '../constants'; -import { getEmailBatch, getAllEmailsSortedNewestFirst, getEmailCount, MailDB } from './indexedDB'; -import { Email, MailCache } from '../types'; -import { emailToBinary } from './utils'; - -/** - * Estimates the email size in the memory - * - * @param email - The email - * @returns The estimation of the email size - */ -function sizeOfEmail(email: Email): number { - return emailToBinary(email).byteLength; -} - -/** - * Creates an empty cache variable - * - * @returns The empty cache - */ -function createEmptyCache(): MailCache { - return { - esCache: new Map(), - cacheSize: 0, - isCacheLimited: false, - isCacheReady: true, - }; -} - -/** - * Fetches all emails from the database in batches and cahces them - * - * @param indexKey - The symmetric key for protecting database - * @param esCache - The cache to add emails too - * @param esDB - The database - */ -export const createCacheFromDB = async (indexKey: Uint8Array, esDB: MailDB): Promise> => { - const esCache = createEmptyCache(); - esCache.isCacheReady = false; - try { - const count = await getEmailCount(esDB); - if (!count) { - esCache.isCacheReady = true; - return esCache; - } - - if (count <= MAX_EMAIL_PER_BATCH) { - const emails = await getAllEmailsSortedNewestFirst(esDB, indexKey); - addEmailsToCache(emails, esCache); - } else { - let nextCursor: IDBValidKey | undefined = undefined; - - let cacheFull = false; - while (!cacheFull) { - const { emails, nextCursor: newCursor }: { emails: Email[]; nextCursor?: IDBValidKey } = await getEmailBatch( - esDB, - indexKey, - MAX_EMAIL_PER_BATCH, - nextCursor, - ); - if (!newCursor || !emails.length) break; - nextCursor = newCursor; - - const success = addEmailsToCache(emails, esCache); - if (!success) cacheFull = true; - } - } - return esCache; - } catch (error) { - throw new Error(`Email caching failed: ${error}`, { cause: error }); - } -}; - -/** - * Gets an email from the cache - * - * @param emailID - The email identifier - * @param esCache - The email cache - * @returns The found email or throws an error - */ -export const getEmailFromCache = async (emailID: string, esCache: MailCache): Promise => { - const email = esCache.esCache.get(emailID); - if (!email) { - throw new Error(`Email not found in cache for ID: ${emailID}`); - } - return email; -}; - -/** - * Removes the email from cache - * - * @param emailID - The email identifier - * @param esCache - The email cache - */ -export const deleteEmailFromCache = async (emailID: string, esCache: MailCache): Promise => { - try { - const email = await getEmailFromCache(emailID, esCache); - const size = sizeOfEmail(email); - const removed = esCache.esCache.delete(emailID); - if (removed) esCache.cacheSize -= size; - } catch (error) { - throw new Error(`Failed to delete email with ID ${emailID}`, { cause: error }); - } -}; - -/** - * Adds emails to the cache - * - * @param emails - The emails to add - * @param esCache - The email cache - * @returns TRUE if all emails were added sucessfully, or FALSE and error reason. - */ -export function addEmailsToCache(emails: Email[], esCache: MailCache): { success: boolean; reason?: string } { - try { - for (const email of emails) { - const result = addEmailToCache(email, esCache); - if (!result.success) return result; - } - return { success: true }; - } catch (error) { - throw new Error('Failed to add emails to the cache', { cause: error }); - } -} - -/** - * Adds email to the cache - * - * @param email - The email to add - * @param esCache - The email cache - * @returns TRUE if the email was added sucessfully, or FALSE and error reason. - */ -export const addEmailToCache = (email: Email, esCache: MailCache): { success: boolean; reason?: string } => { - try { - if (esCache.esCache.has(email.id)) { - return { success: false, reason: 'email already exists in cache' }; - } - - const emailSize = sizeOfEmail(email); - - if (esCache.cacheSize + emailSize > MAX_CACHE_SIZE) { - esCache.isCacheLimited = true; - return { success: false, reason: 'hit cache limit' }; - } - - esCache.esCache.set(email.id, email); - esCache.cacheSize += emailSize; - - return { success: true }; - } catch (error) { - throw new Error('Failed to add email to the cache', { cause: error }); - } -}; diff --git a/src/email-search/search.ts b/src/email-search/search.ts deleted file mode 100644 index 79e61a3..0000000 --- a/src/email-search/search.ts +++ /dev/null @@ -1,207 +0,0 @@ -import { Index, DefaultSearchResults } from 'flexsearch'; -import type { IndexOptions } from 'flexsearch'; -import { Email, MailCache, EmailSearchResult } from '../types'; - -type ExtendedIndexOptions = IndexOptions & { - minlength?: number; - resolution?: number; - optimize?: boolean; - fastupdate?: boolean; - cache?: boolean; -}; - -const SUBJECT_OPTIONS: ExtendedIndexOptions = { - preset: 'match', - tokenize: 'forward', - resolution: 9, - minlength: 2, - optimize: true, - fastupdate: true, - cache: true, -}; -const BODY_OPTIONS: ExtendedIndexOptions = { - ...SUBJECT_OPTIONS, - minlength: 3, - resolution: 6, -}; - -const FROM_OPTIONS: ExtendedIndexOptions = { - ...SUBJECT_OPTIONS, - tokenize: 'strict', - minlength: 1, -}; - -const TO_OPTIONS: ExtendedIndexOptions = { - ...SUBJECT_OPTIONS, - tokenize: 'strict', - minlength: 1, -}; - -export interface EmailSearchIndex { - subjectIndex: Index; - bodyIndex: Index; - fromIndex: Index; - toIndex: Index; - isReady: boolean; -} - -/** - * Creates new email search index - * - * @returns The email search index - */ -const createSearchIndex = (): EmailSearchIndex => ({ - subjectIndex: new Index(SUBJECT_OPTIONS), - bodyIndex: new Index(BODY_OPTIONS), - fromIndex: new Index(FROM_OPTIONS), - toIndex: new Index(TO_OPTIONS), - isReady: false, -}); - -/** - * Adds the given email to the email search index - * - * @param email - The email to add - * @param searchIndex - The email search index - */ -export const addEmailToSearchIndex = (email: Email, searchIndex: EmailSearchIndex): void => { - try { - const emailId = email.id; - searchIndex.subjectIndex.add(emailId, email.body.subject); - searchIndex.bodyIndex.add(emailId, email.body.text); - const senderText = `${email.params.sender.name || ''} ${email.params.sender.email || ''}`.trim(); - searchIndex.fromIndex.add(emailId, senderText); - - const recipientsList = email.params.recipients?.length ? email.params.recipients : [email.params.recipient]; - - const recipientsText = recipientsList - .map((recipient) => `${recipient.name || ''} ${recipient.email || ''}`.trim()) - .join(' '); - searchIndex.toIndex.add(emailId, recipientsText); - } catch (error) { - throw new Error('Failed to add email to the search index', { cause: error }); - } -}; - -/** - * Removes the email from the email search index - * - * @param emailID - The email identifier - * @param searchIndex - The email search index - */ -export const removeEmailFromSearchIndex = (emailID: string, searchIndex: EmailSearchIndex): void => { - try { - searchIndex.subjectIndex.remove(emailID); - searchIndex.bodyIndex.remove(emailID); - searchIndex.fromIndex.remove(emailID); - searchIndex.toIndex.remove(emailID); - } catch (error) { - throw new Error(`Failed to remove email with ID ${emailID} from the search index`, { cause: error }); - } -}; - -/** - * Buils the email search index from the email cache - * - * @param esCache - The email cache - * @returns The email search index - */ -export const buildSearchIndexFromCache = async (esCache: MailCache): Promise => { - try { - const searchIndex = createSearchIndex(); - searchIndex.isReady = false; - - for (const email of esCache.esCache.values()) { - addEmailToSearchIndex(email, searchIndex); - } - - searchIndex.isReady = true; - return searchIndex; - } catch (error) { - throw new Error('Failed build an email search index from the cache', { cause: error }); - } -}; - -export interface SearchOptions { - fields?: ('subject' | 'body' | 'from' | 'to')[]; - limit?: number; - boost?: { - subject?: number; - body?: number; - from?: number; - to?: number; - }; -} - -/** - * Searches in teh - * - * @param query - The string to search for - * @param esCache - The email cache - * @param searchIndex - The email search index - * @param opetions - The optional search limitations - * @returns The result of the email search (emails ans their corresponding result weights) - */ -export const searchEmails = async ( - query: string, - esCache: MailCache, - searchIndex: EmailSearchIndex, - options: SearchOptions = {}, -): Promise => { - try { - if (!searchIndex.isReady || !query.trim()) { - return []; - } - - const { - fields = ['subject', 'body', 'from', 'to'], - limit = 50, - boost = { subject: 3, body: 1, from: 2, to: 2 }, - } = options; - - const results = new Map(); - - const searchPromises = fields.map(async (field) => { - let fieldResults: DefaultSearchResults; - - switch (field) { - case 'subject': - fieldResults = await searchIndex.subjectIndex.searchAsync(query); - break; - case 'body': - fieldResults = await searchIndex.bodyIndex.searchAsync(query); - break; - case 'from': - fieldResults = await searchIndex.fromIndex.searchAsync(query); - break; - case 'to': - fieldResults = await searchIndex.toIndex.searchAsync(query); - break; - default: - return; - } - - const fieldBoost = boost[field] || 1; - - fieldResults.forEach((emailId) => { - const currentScore = results.get(String(emailId)) || 0; - results.set(String(emailId), currentScore + fieldBoost); - }); - }); - - await Promise.all(searchPromises); - - const emailResults: EmailSearchResult[] = []; - - for (const [emailId, score] of results) { - const email = esCache.esCache.get(emailId); - if (email) { - emailResults.push({ email, score }); - } - } - emailResults.sort((a, b) => (b.score || 0) - (a.score || 0)); - return emailResults.slice(0, limit); - } catch (error) { - throw new Error('Email search failed', { cause: error }); - } -}; diff --git a/src/email-search/utils.ts b/src/email-search/utils.ts deleted file mode 100644 index 3267a83..0000000 --- a/src/email-search/utils.ts +++ /dev/null @@ -1,17 +0,0 @@ -import { UTF8ToUint8 } from '../utils'; -import { Email } from '../types'; - -/** - * Converts an Email type into a Uint8Array array. - * - * @param email - The email. - * @returns The Uint8Array array representation of the Email type. - */ -export function emailToBinary(email: Email): Uint8Array { - try { - const json = JSON.stringify(email); - return UTF8ToUint8(json); - } catch (error) { - throw new Error('Failed to convert EmailBody to Uint8Array', { cause: error }); - } -} diff --git a/src/index.ts b/src/index.ts index f75acfb..1022f86 100644 --- a/src/index.ts +++ b/src/index.ts @@ -9,31 +9,6 @@ export { decryptPwdProtectedEmail, generateEmailKeys, } from './email-crypto'; -export { - openDatabase, - closeDatabase, - deriveIndexKey, - encryptAndStoreEmail, - encryptAndStoreManyEmail, - getAndDecryptEmail, - getAndDecryptAllEmails, - deleteEmail, - getEmailCount, - deleteOldestEmails, - enforceMaxEmailNumber, - getAllEmailsSortedNewestFirst, - getAllEmailsSortedOldestFirst, - getEmailBatch, - createCacheFromDB, - getEmailFromCache, - deleteEmailFromCache, - addEmailsToCache, - addEmailToCache, - addEmailToSearchIndex, - removeEmailFromSearchIndex, - buildSearchIndexFromCache, - searchEmails, -} from './email-search'; export { hashDataArray, hashDataArrayWithKey, diff --git a/src/types.ts b/src/types.ts index c3e5893..c28cc77 100644 --- a/src/types.ts +++ b/src/types.ts @@ -30,12 +30,6 @@ export type PwdProtectedEmail = { encEmailBody: EmailBodyEncrypted; }; -export type StoredEmail = { - params: EmailPublicParameters; - encEmailBody: EmailBodyEncrypted; - id: string; -}; - export type HybridEncKey = { hybridCiphertext: string; encryptedKey: string; @@ -59,33 +53,6 @@ export type EmailBody = { attachments?: string[]; }; -export type EmailPublicParameters = { - createdAt: string; - sender: User; - recipients: User[]; - ccs?: User[]; - bccs?: User[]; - replyToEmailID?: string; - labels?: string[]; -}; - -export type Email = { - id: string; - body: EmailBody; - params: EmailPublicParameters; -}; - -export interface MailCache { - esCache: Map; - cacheSize: number; - isCacheLimited: boolean; - isCacheReady: boolean; -} -export interface EmailSearchResult { - email: Email; - score?: number; -} - export enum KeystoreType { ENCRYPTION = 'Encryption', RECOVERY = 'Recovery', diff --git a/tests/email-search/cacheLimit.test.ts b/tests/email-search/cacheLimit.test.ts deleted file mode 100644 index 6cfa7c1..0000000 --- a/tests/email-search/cacheLimit.test.ts +++ /dev/null @@ -1,29 +0,0 @@ -import { describe, it, expect, vi } from 'vitest'; -import { addEmailToCache } from '../../src/email-search'; -import { generateTestEmail } from './helper'; - -vi.mock('../../src/email-search/utils', async () => { - const actual = await vi.importActual('../../src/email-search/utils'); - return { - ...actual, - emailToBinary: () => new Uint8Array(700 * 1024 * 1024), - }; -}); - -describe('Test mail cache limits', () => { - it('should hit cache limit', () => { - const esCacheRef = { - esCache: new Map(), - cacheSize: 0, - isCacheLimited: false, - isCacheReady: true, - }; - - const email = generateTestEmail(); - const result = addEmailToCache(email, esCacheRef); - - expect(result.success).toBe(false); - expect(result.reason).toBe('hit cache limit'); - expect(esCacheRef.isCacheLimited).toBe(true); - }); -}); diff --git a/tests/email-search/helper.ts b/tests/email-search/helper.ts deleted file mode 100644 index a6b4089..0000000 --- a/tests/email-search/helper.ts +++ /dev/null @@ -1,55 +0,0 @@ -import { Email, User } from '../../src/types'; -import { emailToBinary } from '../../src/email-search/utils'; -import { generateUuid } from '../../src/utils'; - -const randomString = (length: number = 8): string => - Math.random() - .toString(36) - .substring(2, 2 + length); - -const randomDate = (): string => new Date(Date.now() - Math.floor(Math.random() * 1e10)).toISOString(); - -const randomUser = (): User => ({ - name: `User_${randomString(4)}`, - email: `${randomString(6)}@example.com`, -}); - -export const generateTestEmail = (data?: string): Email => { - const sender = randomUser(); - - return { - id: generateUuid(), - body: { - text: data ? data : `This is a test email body: ${randomString(20)}`, - subject: `Test Subject ${randomString(6)}`, - ...(Math.random() > 0.5 ? { attachments: [`file_${randomString(4)}.txt`] } : {}), - }, - params: { - createdAt: randomDate(), - sender, - recipients: Math.random() > 0.5 ? [randomUser(), randomUser()] : [randomUser()], - ccs: Math.random() > 0.5 ? [randomUser(), randomUser()] : undefined, - bccs: Math.random() > 0.5 ? [randomUser(), randomUser()] : undefined, - replyToEmailID: generateUuid(), - labels: Math.random() > 0.5 ? ['inbox', 'test'] : undefined, - }, - }; -}; - -export const generateTestEmails = (count: number): Email[] => { - return Array.from({ length: count }, () => generateTestEmail()); -}; - -export function getAllEmailSize(emails: Email[]): number { - return emails.reduce((total, email) => { - return total + emailToBinary(email).byteLength; - }, 0); -} - -export function getEmailSize(email: Email): number { - return emailToBinary(email).byteLength; -} - -export const getSearchTestEmails = (content: string[]): Email[] => { - return content.map((text) => generateTestEmail(text)); -}; diff --git a/tests/email-search/indexedDB.test.ts b/tests/email-search/indexedDB.test.ts deleted file mode 100644 index 0b1d700..0000000 --- a/tests/email-search/indexedDB.test.ts +++ /dev/null @@ -1,163 +0,0 @@ -import { describe, expect, expectTypeOf, it, beforeAll } from 'vitest'; -import { - openDatabase, - encryptAndStoreManyEmail, - encryptAndStoreEmail, - getEmailCount, - getAndDecryptAllEmails, - MailDB, - closeDatabase, - deleteDatabase, - getAndDecryptEmail, - deleteEmail, - getAllEmailsSortedOldestFirst, - deleteOldestEmails, - getAllEmailsSortedNewestFirst, - getEmailBatch, - enforceMaxEmailNumber, - deriveIndexKey, -} from '../../src/email-search'; -import { Email } from '../../src/types'; -import { genSymmetricKey } from '../../src/symmetric-crypto'; -import { generateTestEmails, generateTestEmail } from './helper'; - -describe('Test searchable database functions', async () => { - const emailNumber = 5; - const emails: Email[] = generateTestEmails(emailNumber); - const userID = 'mock ID'; - const key = genSymmetricKey(); - - beforeAll(async () => { - await deleteDatabase(userID); - }); - - it('should sucesfully open the database, add emails and get all emails', async () => { - const db = await openDatabase(userID); - expectTypeOf(db).toEqualTypeOf(); - await encryptAndStoreManyEmail(emails, key, db); - const count = await getEmailCount(db); - expect(count).toBe(emailNumber); - const gotEmails = await getAndDecryptAllEmails(key, db); - expect(emails).toEqual(expect.arrayContaining(gotEmails)); - - closeDatabase(db); - }); - - it('should re-open database and ensure it still has emails', async () => { - const db = await openDatabase(userID); - const count = await getEmailCount(db); - expect(count).toBe(emailNumber); - closeDatabase(db); - }); - - it('should sucessfully get specific email', async () => { - const db = await openDatabase(userID); - const id = emails[0].id; - const email = await getAndDecryptEmail(id, key, db); - expect(email).toStrictEqual(emails[0]); - closeDatabase(db); - }); - - it('should re-open database and ensure it still has emails', async () => { - const db = await openDatabase(userID); - const count = await getEmailCount(db); - expect(count).toBe(emailNumber); - closeDatabase(db); - }); - - it('should sucessfully delete specific email', async () => { - const db = await openDatabase(userID); - const id = emails[0].id; - await deleteEmail(id, db); - const count = await getEmailCount(db); - expect(count).toBe(emailNumber - 1); - const gotEmails = await getAndDecryptAllEmails(key, db); - expect(gotEmails.some((email) => email.id === id)).toBe(false); - closeDatabase(db); - }); - - it('should sucessfully add one email to existing database', async () => { - const db = await openDatabase(userID); - const email = generateTestEmail(); - await encryptAndStoreEmail(email, key, db); - const count = await getEmailCount(db); - expect(count).toBe(emailNumber); - const gotEmails = await getAndDecryptAllEmails(key, db); - expect(gotEmails).toContainEqual(email); - closeDatabase(db); - }); - - it('should not change database if the same email added again', async () => { - const db = await openDatabase(userID); - const email = generateTestEmail(); - await encryptAndStoreEmail(email, key, db); - const count = await getEmailCount(db); - expect(count).toBe(emailNumber + 1); - await encryptAndStoreEmail(email, key, db); - const newCount = await getEmailCount(db); - expect(newCount).toBe(emailNumber + 1); - closeDatabase(db); - }); - - it('should sucessfully delete oldest emails ', async () => { - const number = 2; - const db = await openDatabase(userID); - const emails = await getAllEmailsSortedOldestFirst(db, key); - await deleteOldestEmails(number, db); - const allEmails = await getAndDecryptAllEmails(key, db); - for (let i = 0; i < number; i++) { - expect(allEmails).not.toContainEqual(emails[i]); - } - - closeDatabase(db); - }); - - it('should sucessfully get email batch', async () => { - const batchSize = 3; - const db = await openDatabase(userID); - const allEmails = await getAllEmailsSortedNewestFirst(db, key); - const batchedEmails: Email[] = []; - let nextCursor: IDBValidKey | undefined; - - do { - const result = await getEmailBatch(db, key, batchSize, nextCursor); - batchedEmails.push(...result.emails); - nextCursor = result.nextCursor; - expect(result.emails.length).toBeLessThanOrEqual(batchSize); - } while (nextCursor); - - expect(batchedEmails.length).toBe(allEmails.length); - expect(batchedEmails).toStrictEqual(allEmails); - - closeDatabase(db); - }); - - it('should sucessfully set max email number ', async () => { - const number = 7; - const db = await openDatabase(userID); - const emails = generateTestEmails(number); - await encryptAndStoreManyEmail(emails, key, db); - const count = await getEmailCount(db); - expect(count).not.toBe(number); - await enforceMaxEmailNumber(db, number); - const newCount = await getEmailCount(db); - expect(newCount).toBe(number); - - closeDatabase(db); - }); - - it('after deling the database and opening it, email count is 0 ', async () => { - await deleteDatabase(userID); - const db = await openDatabase(userID); - const count = await getEmailCount(db); - expect(count).toBe(0); - closeDatabase(db); - }); - - it('derive index key should work', async () => { - const baseKey = genSymmetricKey(); - const newKey = await deriveIndexKey(baseKey); - - expect(newKey).toBeInstanceOf(Uint8Array); - }); -}); diff --git a/tests/email-search/mailCache.test.ts b/tests/email-search/mailCache.test.ts deleted file mode 100644 index 604efc5..0000000 --- a/tests/email-search/mailCache.test.ts +++ /dev/null @@ -1,141 +0,0 @@ -import { describe, it, expect, beforeAll, afterAll } from 'vitest'; -import { - addEmailToCache, - addEmailsToCache, - getEmailFromCache, - deleteEmailFromCache, - deleteDatabase, - openDatabase, - encryptAndStoreManyEmail, - closeDatabase, - createCacheFromDB, - getEmailCount, - MailDB, -} from '../../src/email-search'; -import { Email } from '../../src/types'; -import { genSymmetricKey } from '../../src/symmetric-crypto'; -import { generateTestEmails, generateTestEmail, getAllEmailSize, getEmailSize } from './helper'; - -describe('Test mail cache functions', () => { - beforeAll(async () => { - await deleteDatabase(userID); - key = genSymmetricKey(); - db = await openDatabase(userID); - await encryptAndStoreManyEmail(emails, key, db); - }); - - afterAll(async () => { - closeDatabase(db); - }); - - const emailNumber = 5; - const emails: Email[] = generateTestEmails(emailNumber); - const userID = 'mock ID'; - let db: MailDB; - let key: Uint8Array; - - it('cacheEmailsFromIDB sucessfully reads emails form database', async () => { - const esCache = await createCacheFromDB(key, db); - const totalSize = getAllEmailSize(emails); - - const count = await getEmailCount(db); - expect(count).toBe(emailNumber); - - expect(esCache.esCache.size).toBe(emailNumber); - expect(esCache.cacheSize).toBe(totalSize); - expect(esCache.esCache.get(emails[0].id)).toEqual(emails[0]); - }); - - it('addEmailToCache adds an email and updates size', async () => { - const email = generateTestEmail(); - const esCache = await createCacheFromDB(key, db); - const sizeBefore = esCache.cacheSize; - const result = addEmailToCache(email, esCache); - const diff = esCache.cacheSize - sizeBefore; - const emailSize = getEmailSize(email); - - expect(result.success).toBe(true); - expect(diff).toBe(emailSize); - expect(esCache.esCache.size).toBe(emailNumber + 1); - }); - - it('addEmailToCache will not add the same email twice', async () => { - const email = generateTestEmail(); - const esCache = await createCacheFromDB(key, db); - const result = addEmailToCache(email, esCache); - expect(result.success).toBe(true); - - const sizeBeforeSecondInsert = esCache.esCache.size; - expect(sizeBeforeSecondInsert).toBe(emailNumber + 1); - - const resultRepeated = addEmailToCache(email, esCache); - - expect(resultRepeated.success).toBe(false); - expect(esCache.esCache.size).toBe(sizeBeforeSecondInsert); - }); - - it('addEmailsToCache adds multiple emails', async () => { - const number = 3; - const esCache = await createCacheFromDB(key, db); - const emails = generateTestEmails(number); - const before = esCache.cacheSize; - const sizeBefore = esCache.esCache.size; - const result = addEmailsToCache(emails, esCache); - - const after = esCache.cacheSize - before; - const inserted = esCache.esCache.size - sizeBefore; - const size = getAllEmailSize(emails); - - expect(result.success).toBe(true); - expect(inserted).toBe(number); - expect(after).toBe(size); - }); - - it('getEmailFromCache retrieves an email by id', async () => { - const email = emails[0]; - const esCache = await createCacheFromDB(key, db); - const got = await getEmailFromCache(email.id, esCache); - - expect(got).toStrictEqual(email); - }); - - it('deleteEmailFromCache removes an email and updates size', async () => { - const esCache = await createCacheFromDB(key, db); - const sizeBefore = esCache.esCache.size; - const cacheBefore = esCache.cacheSize; - const email = emails[0]; - const emailSize = getEmailSize(email); - await deleteEmailFromCache(email.id, esCache); - expect(esCache.esCache.size).toBe(sizeBefore - 1); - expect(esCache.cacheSize).toBe(cacheBefore - emailSize); - }); - - it('cacheEmailsFromIDB should work for an empty database', async () => { - const id = 'non-existant-user'; - const emptyDB = await openDatabase(id); - const cache = await createCacheFromDB(key, emptyDB); - const count = await getEmailCount(emptyDB); - - expect(count).toBe(0); - expect(cache.esCache.size).toBe(0); - expect(cache.cacheSize).toBe(0); - - closeDatabase(emptyDB); - deleteDatabase(id); - }); - - it('cacheEmailsFromIDB should work with batches', async () => { - const id = 'big-db'; - const number = 200; - const manyEmails = generateTestEmails(number); - const totalSize = getAllEmailSize(manyEmails); - const bigDB = await openDatabase(id); - await encryptAndStoreManyEmail(manyEmails, key, bigDB); - const cache = await createCacheFromDB(key, bigDB); - const count = await getEmailCount(bigDB); - - expect(count).toBe(number); - expect(cache.esCache.size).toBe(number); - expect(cache.cacheSize).toBe(totalSize); - }); -}); diff --git a/tests/email-search/search.test.ts b/tests/email-search/search.test.ts deleted file mode 100644 index ee59ddf..0000000 --- a/tests/email-search/search.test.ts +++ /dev/null @@ -1,73 +0,0 @@ -import { describe, it, expect, beforeAll, afterAll } from 'vitest'; -import { - openDatabase, - searchEmails, - buildSearchIndexFromCache, - encryptAndStoreManyEmail, - createCacheFromDB, - deleteDatabase, - closeDatabase, - MailDB, -} from '../../src/email-search'; -import { Email } from '../../src/types'; -import { genSymmetricKey } from '../../src/symmetric-crypto'; -import { generateTestEmails, getSearchTestEmails } from './helper'; - -describe('Email Search', () => { - beforeAll(async () => { - await deleteDatabase(userID); - key = genSymmetricKey(); - db = await openDatabase(userID); - await encryptAndStoreManyEmail(emails, key, db); - }); - - afterAll(async () => { - closeDatabase(db); - }); - - const emailNumber = 5; - const emails: Email[] = generateTestEmails(emailNumber); - const userID = 'mock ID'; - let db: MailDB; - let key: Uint8Array; - - it('should build search index from cache', async () => { - const esCache = await createCacheFromDB(key, db); - const searchIndex = await buildSearchIndexFromCache(esCache); - - const result = await searchEmails('Test Subject', esCache, searchIndex); - - expect(result.length).toBe(emailNumber); - }); - - it('should search sucessfully', async () => { - const id = 'test user id'; - const indexKey = genSymmetricKey(); - const database = await openDatabase(id); - const data = [ - 'cats abcd efgh ijkl mnop qrst uvwx', - 'cats abcd efgh ijkl mnop qrst ', - 'cats abcd efgh ijkl mnop cute', - 'cats abcd efgh ijkl', - 'cats abcd efgh cute', - 'cats abcd', - 'cats cute', - ]; - const testEmails = getSearchTestEmails(data); - await encryptAndStoreManyEmail(testEmails, indexKey, database); - const cache = await createCacheFromDB(indexKey, database); - const search = await buildSearchIndexFromCache(cache); - - const result = await searchEmails('cats cute', cache, search); - - expect(result.length).toBe(3); - - const resultInSubjectsOnly = await searchEmails('cats cute', cache, search, { - fields: ['subject'], - limit: 5, - }); - expect(resultInSubjectsOnly.length).toBe(0); - - closeDatabase(database); - }); -}); diff --git a/tsdown.config.ts b/tsdown.config.ts index 85ccd3e..236b1b3 100644 --- a/tsdown.config.ts +++ b/tsdown.config.ts @@ -11,7 +11,6 @@ export default defineConfig({ 'derive-password': 'src/derive-password/index.ts', 'email-crypto': 'src/email-crypto/index.ts', 'keystore-crypto': 'src/keystore-crypto/index.ts', - 'email-search': 'src/email-search/index.ts', 'storage-service': 'src/storage-service/index.ts', utils: 'src/utils/index.ts', types: 'src/types.ts', diff --git a/yarn.lock b/yarn.lock index d402fd2..ab10d9e 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1602,11 +1602,6 @@ flatted@^3.2.9: resolved "https://registry.yarnpkg.com/flatted/-/flatted-3.3.3.tgz#67c8fad95454a7c7abebf74bb78ee74a44023358" integrity sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg== -flexsearch@^0.8.205: - version "0.8.212" - resolved "https://registry.yarnpkg.com/flexsearch/-/flexsearch-0.8.212.tgz#b9509af778a991b938292e36fe0809a4ece4b940" - integrity sha512-wSyJr1GUWoOOIISRu+X2IXiOcVfg9qqBRyCPRUdLMIGJqPzMo+jMRlvE83t14v1j0dRMEaBbER/adQjp6Du2pw== - formatly@^0.3.0: version "0.3.0" resolved "https://registry.yarnpkg.com/formatly/-/formatly-0.3.0.tgz#5bb3b4e692f5a8c74ad8fe26154dd0a74aac6819" @@ -1680,11 +1675,6 @@ husky@^9.1.7: resolved "https://registry.yarnpkg.com/husky/-/husky-9.1.7.tgz#d46a38035d101b46a70456a850ff4201344c0b2d" integrity sha512-5gs5ytaNjBrh5Ow3zrvdUUY+0VxIuWVL4i9irt6friV+BqdCfmV11CQTWMiBYWHbXhco+J1kHfTOUkePhCDvMA== -idb@^8.0.3: - version "8.0.3" - resolved "https://registry.yarnpkg.com/idb/-/idb-8.0.3.tgz#c91e558f15a8d53f1d7f53a094d226fc3ad71fd9" - integrity sha512-LtwtVyVYO5BqRvcsKuB2iUMnHwPVByPCXFXOpuU96IZPPoPN6xjOGxZQ74pgSVVLQWtUOYgyeL4GE98BY5D3wg== - ignore@^5.2.0: version "5.3.2" resolved "https://registry.yarnpkg.com/ignore/-/ignore-5.3.2.tgz#3cd40e729f3643fd87cb04e50bf0eb722bc596f5"