diff --git a/TODO.md b/TODO.md index f244f99..738109d 100644 --- a/TODO.md +++ b/TODO.md @@ -1,5 +1,7 @@ ### Todo +- [ ] implement CRON logic in normal extension use + ### In Progress - [ ] Widget to show unopened emails diff --git a/chrome-extension/js/inject.js b/chrome-extension/js/inject.js index c016719..e9cb5ec 100644 --- a/chrome-extension/js/inject.js +++ b/chrome-extension/js/inject.js @@ -1,5 +1,5 @@ // Configuration and Logger -const isDev = false; +const isDev = true; const DOMAIN = "stealthbyte.deno.dev"; const LOCALHOST = "localhost:8080"; const baseTrackingPixelUrl = isDev ? `http://${LOCALHOST}` : `https://${DOMAIN}`; diff --git a/chrome-extension/js/popup.js b/chrome-extension/js/popup.js new file mode 100644 index 0000000..d27718a --- /dev/null +++ b/chrome-extension/js/popup.js @@ -0,0 +1,31 @@ +chrome.runtime.sendMessage({message: "fetch_user_data"}, function(response) { + if (response.error) { + console.error('Error:', response.error); + document.body.innerHTML = '

Error loading data. Please try again later.

'; + } else { + displayUserInfo(response.userData); + displayEmails(response.userEmails); + } +}); + +function displayUserInfo(userData) { + const userInfoDiv = document.getElementById('user-info'); + userInfoDiv.innerHTML = ` +

Email: ${userData.email}

+

Emails sent this month: ${userData.emailsSentThisMonth}

+ `; +} + +function displayEmails(emails) { + const emailListDiv = document.getElementById('email-list'); + emails.forEach(email => { + const emailDiv = document.createElement('div'); + emailDiv.className = 'email-item'; + emailDiv.innerHTML = ` +
${email.subject}
+
Opened: ${email.numberOfOpens} times
+
Sent: ${new Date(Number(email.dateAtTimeOfSend)).toLocaleString()}
+ `; + emailListDiv.appendChild(emailDiv); + }); +} \ No newline at end of file diff --git a/chrome-extension/js/service-worker.js b/chrome-extension/js/service-worker.js index 9e20cb1..ef32a8a 100644 --- a/chrome-extension/js/service-worker.js +++ b/chrome-extension/js/service-worker.js @@ -17,8 +17,8 @@ const logger = { let authToken = null; const DOMAIN = "stealthbyte.deno.dev"; const LOCALHOST = "localhost:8080"; -const isDev = false; -const serverUrl = isDev ? `http://${LOCALHOST}` : `https://${DOMAIN}`; +const isDev = true; +const SERVER_URL = isDev ? `http://${LOCALHOST}` : `https://${DOMAIN}`; // Function to get OAuth2 token function getAuthToken(interactive) { return new Promise((resolve, reject) => { @@ -141,6 +141,10 @@ chrome.runtime.onMessage.addListener((request, sender, sendResponse) => { logger.info("Processing email data:", request.data.subject); processEmail(request.data, sender); // Pass sender to processEmail } + if (request.message === "fetch_user_data") { + fetchUserData().then(sendResponse); + return true; // Indicates that the response is sent asynchronously + } }); // Function to process email data and initiate retry mechanism @@ -282,7 +286,7 @@ chrome.alarms.onAlarm.addListener(async (alarm) => { // Send a notification to the server try { const serverResponse = await fetch( - `${serverUrl}/${uniqueId}/pixel.png`, + `${SERVER_URL}/${uniqueId}/pixel.png`, { method: "POST", headers: { @@ -333,3 +337,88 @@ chrome.alarms.onAlarm.addListener(async (alarm) => { logger.error(`Error handling alarm "${sleepId}":`, error.message); } }); + +async function fetchUserData() { + logger.info("Fetching user data..."); + try { + const token = await ensureAuthenticated(); + logger.info("Authentication token obtained"); + + const cacheStatus = await fetchCacheStatus(token); + logger.info("Cache status:", cacheStatus); + + if (!cacheStatus.cached) { + logger.info("Cache is invalid, fetching new data from server"); + const data = await fetchAllEmailData(token); + logger.info("New data fetched from server"); + await storeDataInLocalStorage(data); + logger.info("New data stored in local storage"); + return data; + } else { + logger.info("Cache is valid, retrieving data from local storage"); + return await getDataFromLocalStorage(); + } + } catch (error) { + logger.error('Error in fetchUserData:', error); + return { error: 'Failed to fetch user data' }; + } +} + +async function fetchCacheStatus(token) { + logger.info("Fetching cache status from server"); + try { + const response = await fetch(`${SERVER_URL}/cache-status`, { + headers: { + 'Authorization': `Bearer ${token}` + } + }); + const data = await response.json(); + logger.info("Cache status received:", data); + return data; + } catch (error) { + logger.error('Error fetching cache status:', error); + throw error; + } +} + +async function fetchAllEmailData(token) { + logger.info("Fetching all email data from server"); + try { + const response = await fetch(`${SERVER_URL}/all-email-data`, { + headers: { + 'Authorization': `Bearer ${token}` + } + }); + const data = await response.json(); + logger.info("All email data received"); + return data; + } catch (error) { + logger.error('Error fetching all email data:', error); + throw error; + } +} + +async function storeDataInLocalStorage(data) { + logger.info("Storing data in local storage"); + return new Promise((resolve) => { + chrome.storage.local.set({ 'userData': data }, () => { + logger.info("Data stored in local storage"); + resolve(); + }); + }); +} + +async function getDataFromLocalStorage() { + logger.info("Retrieving data from local storage"); + return new Promise((resolve) => { + chrome.storage.local.get('userData', (result) => { + if (result.userData) { + logger.info("Data retrieved from local storage"); + resolve(result.userData); + } else { + logger.warn("No data found in local storage"); + resolve(null); + } + }); + }); +} diff --git a/chrome-extension/manifest.json b/chrome-extension/manifest.json index 13f72f4..7fb5d6f 100644 --- a/chrome-extension/manifest.json +++ b/chrome-extension/manifest.json @@ -30,5 +30,8 @@ "https://www.googleapis.com/auth/userinfo.profile" ] }, - "key": "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAo+GASpGqfyteWsX4CNqnyRpsDuH/ZbIfQemxI8mJifK2NZC+VsxBTULNsJj+V8vgFeTu5N0R2Gz5mTgiYnzRIo2xUfWUeNnC2xS2XigBYkGBziY7cT/eHFTT5x7pinEKK/FwByhlYfwHm6lOlrqWQJCnliUSfHMRCeVakh7hB6V7G2tt/CohZhmNKxPx3azsqSMq1zhtZc5jzSYcomfDO5SdO1mJ3ZXMweAERXs0AAYJ8vneDc0FzkPIeQVipSQhxSqeObtAK9jaM1SPaIZSoQ997+mrkRXXbvYhNBtiQ/eHoNIovarim0eYLbi80OfLhFqX6EYJGSFLHjYSHlpVeQIDAQAB" -} \ No newline at end of file + "key": "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAo+GASpGqfyteWsX4CNqnyRpsDuH/ZbIfQemxI8mJifK2NZC+VsxBTULNsJj+V8vgFeTu5N0R2Gz5mTgiYnzRIo2xUfWUeNnC2xS2XigBYkGBziY7cT/eHFTT5x7pinEKK/FwByhlYfwHm6lOlrqWQJCnliUSfHMRCeVakh7hB6V7G2tt/CohZhmNKxPx3azsqSMq1zhtZc5jzSYcomfDO5SdO1mJ3ZXMweAERXs0AAYJ8vneDc0FzkPIeQVipSQhxSqeObtAK9jaM1SPaIZSoQ997+mrkRXXbvYhNBtiQ/eHoNIovarim0eYLbi80OfLhFqX6EYJGSFLHjYSHlpVeQIDAQAB", + "action": { + "default_popup": "popup.html" + } +} diff --git a/chrome-extension/popup.html b/chrome-extension/popup.html new file mode 100644 index 0000000..53dcd07 --- /dev/null +++ b/chrome-extension/popup.html @@ -0,0 +1,19 @@ + + + + Email Tracker Summary + + + +

Email Tracker Summary

+
+

Tracked Emails

+
+ + + \ No newline at end of file diff --git a/web-server/html/home.ts b/web-server/html/home.ts index 6abbea3..aaf7336 100644 --- a/web-server/html/home.ts +++ b/web-server/html/home.ts @@ -71,8 +71,10 @@ export const homePageHtml = ` View Privacy Policy -`; \ No newline at end of file +`; diff --git a/web-server/html/privacy.ts b/web-server/html/privacy.ts index ad38504..b544e24 100644 --- a/web-server/html/privacy.ts +++ b/web-server/html/privacy.ts @@ -114,4 +114,4 @@ export const privacyPolicyHtml = ` -`; \ No newline at end of file +`; diff --git a/web-server/main.ts b/web-server/main.ts index f7865d9..2309bbb 100644 --- a/web-server/main.ts +++ b/web-server/main.ts @@ -7,7 +7,7 @@ import { extractNamesAndEmails, formatDate, getUserData, - listAllKeysAndValues, + getUserEmailsSorted, returnImage, updateUserData, } from "@utils"; @@ -21,7 +21,8 @@ const subjectMapping: { [key: number]: string } = { 2: "Twice the attention! Your email '{subject}' was opened for the second time.", 3: "Your email '{subject}' has caught their eye for the third time!", 10: "10th open milestone—your email '{subject}' is making waves!", - 20: "Your email '{subject}' has been revisited 20 times—keep the momentum going!", + 20: + "Your email '{subject}' has been revisited 20 times—keep the momentum going!", 50: "Half a century of opens! Your email '{subject}' is a hit.", }; @@ -187,7 +188,9 @@ router.get("/", (ctx) => { if (timeDifferenceInSeconds <= thresholdInSeconds) { // The request came in too soon after sending the email console.log( - `Request came in ${timeDifferenceInSeconds.toFixed(2)} seconds after sending email. Not counting as an open.` + `Request came in ${ + timeDifferenceInSeconds.toFixed(2) + } seconds after sending email. Not counting as an open.`, ); returnImage(ctx); return; @@ -231,12 +234,15 @@ router.get("/", (ctx) => { const emailLink = `https://mail.google.com/mail/u/${userIndex}/#inbox/${emailData.email_id}`; - const emailSubject = subjectMapping[numberOfOpens].replace('{subject}', emailData.subject); + const emailSubject = subjectMapping[numberOfOpens].replace( + "{subject}", + emailData.subject, + ); const recipients = extractNamesAndEmails(emailData.recipient); - const recipientList = recipients.map(recipient => { + const recipientList = recipients.map((recipient) => { return `${recipient.name} ${recipient.email}`; - }).join(', '); + }).join(", "); const emailFrom = `Email-Tracker { // Update user data after successful send userData.emailsSentThisMonth += 1; + // Mark the user data as not cached to force frontend to refresh + userData.cached = false; await updateUserData(userData, kv); } catch (error) { // Log the error and proceed @@ -297,7 +305,35 @@ router.get("/", (ctx) => { // ctx.response.status = 500; // ctx.response.body = { error: "Internal server error" }; } +}) +// endpoint to check if the user data has been updated since last being cached +.get("/cache-status", authorizationMiddleware, async (ctx) => { + // Get the user's cache status + console.log("Checking cache status for user:", ctx.state.email); + const email = ctx.state.email; + const userData = await getUserData(email, kv); + console.log("Cache status:", userData.cached); + ctx.response.body = { cached: userData.cached }; + ctx.response.status = 200; + ctx.response.headers.set("Content-Type", "application/json"); +}) +// endpoint to fetch all email data for a given user +.get("/all-email-data", authorizationMiddleware, async (ctx) => { + // Get all email data for the user + console.log("Fetching all email data for user:", ctx.state.email); + const email = ctx.state.email; + const userData = await getUserData(email, kv); + const userEmails = await getUserEmailsSorted(email, kv); + console.log("All email data:", userEmails); + console.log("User data:", userData); + // Mark the user data as cached + userData.cached = true; + await updateUserData(userData, kv); + ctx.response.body = { userEmails, userData }; + ctx.response.status = 200; + ctx.response.headers.set("Content-Type", "application/json"); }); + router.post("/:uuid/pixel.png", authorizationMiddleware, async (ctx) => { try { const body = await ctx.request.body.json(); @@ -308,9 +344,18 @@ router.post("/:uuid/pixel.png", authorizationMiddleware, async (ctx) => { numberOfOpens: 0, storedAt: timestamp, }; - console.log("email_key: ", email_path_key); + console.log("email_path_key: ", email_path_key); console.log("body: ", body); + + // Store the email data await kv.set(["emailData", email_path_key], emailData); + + // Update the user's cache status + const senderEmail = ctx.state.email; + const userData = await getUserData(senderEmail, kv); + userData.cached = false; + await updateUserData(userData, kv); + ctx.response.status = 200; ctx.response.body = { message: "Email data stored successfully" }; } catch (error) { diff --git a/web-server/utils/types.ts b/web-server/utils/types.ts index 1b9872b..370b156 100644 --- a/web-server/utils/types.ts +++ b/web-server/utils/types.ts @@ -13,4 +13,5 @@ export interface UserData { email: string; emailsSentThisMonth: number; lastReset: number; + cached: boolean; } diff --git a/web-server/utils/utils.ts b/web-server/utils/utils.ts index 766f9e9..01cf0e1 100644 --- a/web-server/utils/utils.ts +++ b/web-server/utils/utils.ts @@ -30,6 +30,7 @@ export async function getUserData( email: email.toLowerCase(), emailsSentThisMonth: 0, lastReset: now, + cached: false, }; } @@ -41,6 +42,32 @@ export async function updateUserData(userData: UserData, kv: Deno.Kv) { await kv.set(userKey, userData); } +export async function getUserEmailsSorted( + email: string, + kv: Deno.Kv, +): Promise { + const lowercaseEmail = email.toLowerCase(); + const iterator = kv.list({ prefix: ["emailData"] }); + const senderCache = new Map(); + + return await Array.fromAsync(iterator) + .then((entries) => + entries + .map((entry) => entry.value as EmailData) + .filter((emailData) => { + let senderEmail: string; + if (senderCache.has(emailData.sender)) { + senderEmail = senderCache.get(emailData.sender)!; + } else { + senderEmail = extractNamesAndEmails(emailData.sender)[0].email.toLowerCase(); + senderCache.set(emailData.sender, senderEmail); + } + return senderEmail === lowercaseEmail; + }) + .sort((a, b) => Number(b.dateAtTimeOfSend) - Number(a.dateAtTimeOfSend)) + ); +} + export const extractNamesAndEmails = ( input: string, ): Array<{ name: string; email: string }> => {