diff --git a/browser-extensions/chrome/src/pages/EmailsList.tsx b/browser-extensions/chrome/src/pages/EmailsList.tsx index 0cb8da279..db74552e0 100644 --- a/browser-extensions/chrome/src/pages/EmailsList.tsx +++ b/browser-extensions/chrome/src/pages/EmailsList.tsx @@ -1,12 +1,12 @@ import React, { useEffect, useState, useCallback } from 'react'; -import { MailboxBulkResponse, MailboxEmailApiModel } from '../models/email'; +import { MailboxBulkRequest, MailboxBulkResponse } from '../types/webapi/MailboxBulk'; +import { MailboxEmail } from '../types/webapi/MailboxEmail'; import { useDb } from '../context/DbContext'; import { useWebApi } from '../context/WebApiContext'; -import { MailboxBulkRequest } from '../types/webapi/MailboxBulk'; import LoadingSpinner from '../components/LoadingSpinner'; -import React from 'react'; import { useMinDurationLoading } from '../hooks/useMinDurationLoading'; - +import EncryptionUtility from '../utils/EncryptionUtility'; +import { Buffer } from 'buffer'; /** * Emails list page. */ @@ -14,7 +14,7 @@ const EmailsList: React.FC = () => { const dbContext = useDb(); const webApi = useWebApi(); const [error, setError] = useState(null); - const [emails, setEmails] = useState([]); + const [emails, setEmails] = useState([]); const [currentPage, setCurrentPage] = useState(1); const [pageSize] = useState(50); const [totalRecords, setTotalRecords] = useState(0); @@ -48,9 +48,31 @@ const EmailsList: React.FC = () => { addresses: emailAddresses, page: currentPage, pageSize: pageSize, - }); + }) as MailboxBulkResponse; - setEmails(data.mails); + // Decrypt emails locally using private key associated with the email address. + const encryptionKeys = dbContext.sqliteClient.getAllEncryptionKeys(); + + const decryptedEmails = await Promise.all(data.mails.map(async email => { + const encrytionKey = encryptionKeys.find(key => key.PublicKey === email.encryptionKey); + if (!encrytionKey) { + throw new Error(`Encryption key not found for email: ${email.fromDisplay}`); + } + + // Decrypt symmetric key with assymetric private key. + const symmetricKey = await EncryptionUtility.decryptWithPrivateKey(email.encryptedSymmetricKey, encrytionKey.PrivateKey); + const symmetricKeyBase64 = Buffer.from(symmetricKey).toString('base64'); + + // Decrypt email with decrypted symmetric key. + email.subject = await EncryptionUtility.symmetricDecrypt(email.subject, symmetricKeyBase64); + email.fromDisplay = await EncryptionUtility.symmetricDecrypt(email.fromDisplay, symmetricKeyBase64); + email.fromDomain = await EncryptionUtility.symmetricDecrypt(email.fromDomain, symmetricKeyBase64); + email.fromLocal = await EncryptionUtility.symmetricDecrypt(email.fromLocal, symmetricKeyBase64); + email.messagePreview = await EncryptionUtility.symmetricDecrypt(email.messagePreview, symmetricKeyBase64); + return email; + })); + + setEmails(decryptedEmails); setTotalRecords(data.totalRecords); } catch (error) { console.error(error); @@ -61,12 +83,42 @@ const EmailsList: React.FC = () => { } finally { setIsLoading(false); } - }, [currentPage, dbContext?.sqliteClient, pageSize, webApi]); + }, [currentPage, dbContext?.sqliteClient, pageSize, webApi, setIsLoading]); useEffect(() => { loadEmails(); }, [loadEmails]); + /** + * Formats the date display for emails + */ + const formatEmailDate = (dateSystem: string): string => { + const now = new Date(); + const emailDate = new Date(dateSystem); + const secondsAgo = Math.floor((now.getTime() - emailDate.getTime()) / 1000); + + if (secondsAgo < 60) { + return 'just now'; + } else if (secondsAgo < 3600) { + // Less than 1 hour ago + const minutes = Math.floor(secondsAgo / 60); + return `${minutes} ${minutes === 1 ? 'minute' : 'minutes'} ago`; + } else if (secondsAgo < 86400) { + // Less than 24 hours ago + const hours = Math.floor(secondsAgo / 3600); + return `${hours} ${hours === 1 ? 'hour' : 'hours'} ago`; + } else if (secondsAgo < 172800) { + // Less than 48 hours ago + return 'yesterday'; + } else { + // Older than 48 hours + return emailDate.toLocaleDateString('en-GB', { + day: '2-digit', + month: '2-digit' + }); + } + }; + if (isLoading) { return (
@@ -91,37 +143,28 @@ const EmailsList: React.FC = () => { return (

Emails

-
- - - - - - - - - - {emails.map((email) => ( - - - - - - ))} - -
- Subject - - From - - Date -
- {email.subject} - - {email.fromDisplay} - - {new Date(email.dateSystem).toLocaleString()} -
+
+ {emails.map((email) => ( +
+
+
+ {email.fromDisplay} +
+
+ {formatEmailDate(email.dateSystem)} +
+
+
+ {email.subject} +
+
+ {email.messagePreview} +
+
+ ))}
); diff --git a/browser-extensions/chrome/src/types/EncryptionKey.ts b/browser-extensions/chrome/src/types/EncryptionKey.ts new file mode 100644 index 000000000..39b847985 --- /dev/null +++ b/browser-extensions/chrome/src/types/EncryptionKey.ts @@ -0,0 +1,6 @@ +export type EncryptionKey = { + Id: string; + PublicKey: string; + PrivateKey: string; + IsPrimary: boolean; +} diff --git a/browser-extensions/chrome/src/utils/EncryptionUtility.tsx b/browser-extensions/chrome/src/utils/EncryptionUtility.tsx index abc34b5f9..72ce06165 100644 --- a/browser-extensions/chrome/src/utils/EncryptionUtility.tsx +++ b/browser-extensions/chrome/src/utils/EncryptionUtility.tsx @@ -178,7 +178,6 @@ class EncryptionUtility { ); const cipherBuffer = Uint8Array.from(atob(ciphertext), c => c.charCodeAt(0)); - const plaintextBuffer = await crypto.subtle.decrypt( { name: "RSA-OAEP", diff --git a/browser-extensions/chrome/src/utils/SqliteClient.tsx b/browser-extensions/chrome/src/utils/SqliteClient.tsx index 304d53351..86b0208f2 100644 --- a/browser-extensions/chrome/src/utils/SqliteClient.tsx +++ b/browser-extensions/chrome/src/utils/SqliteClient.tsx @@ -1,5 +1,6 @@ import initSqlJs, { Database } from 'sql.js'; import { Credential } from '../types/Credential'; +import { EncryptionKey } from '../types/EncryptionKey'; /** * Client for interacting with the SQLite database. @@ -116,6 +117,17 @@ class SqliteClient { GROUP BY c.Id, c.Username, c.ServiceId, s.Name, s.Url, s.Logo `); } + + /** + * Fetch all encryption keys. + */ + public getAllEncryptionKeys(): EncryptionKey[] { + return this.executeQuery(`SELECT + x.PublicKey, + x.PrivateKey, + x.IsPrimary + FROM EncryptionKeys x`); + } } export default SqliteClient; \ No newline at end of file