Skip to content

Conversation

@hoangsvit
Copy link
Member

@hoangsvit hoangsvit commented Dec 30, 2025

User description

Description

What's new?

PR Type

What kind of change does this PR introduce?

  • Bugfix
  • Feature
  • Code style update (formatting, local variables)
  • Refactoring (no functional changes, no api changes)
  • Build related changes
  • CI related changes
  • Documentation content changes
  • Tests
  • Other

Screenshots


PR Type

Enhancement, Bug fix


Description

  • Implement account-specific storage for email history, favorites, and statistics

  • Add badge counter display with configurable modes (total, today, week, all-time)

  • Enhance favorites management with star/unstar functionality and dedicated view mode

  • Add pagination and filtering for recent aliases with improved UI

  • Implement email account management with edit, delete, and switch capabilities

  • Refactor settings UI with improved layout, tabs, and account management section

  • Increase default history limit from 5 to 20 aliases

  • Add data migration from old format to account-specific storage format


Diagram Walkthrough

flowchart LR
  A["Old Storage<br/>Single Format"] -->|"Migrate Data"| B["Account-Specific<br/>Storage Keys"]
  C["Email Accounts<br/>Management"] -->|"Switch/Edit"| B
  D["Badge Counter<br/>Settings"] -->|"Update Display"| E["Extension Badge"]
  F["Favorites<br/>Management"] -->|"Star/Unstar"| B
  G["Pagination &<br/>Filtering"] -->|"Display Aliases"| H["Recent Aliases<br/>View"]
  B -->|"Load Data"| H
  B -->|"Load Data"| I["Statistics<br/>Component"]
Loading

File Walkthrough

Relevant files
Enhancement
background.ts
Add badge counter and account-specific storage handling   

entrypoints/background.ts

  • Refactored context menu creation into separate async function that
    loads custom presets from storage
  • Added storage change listener to recreate context menus and update
    badge when settings change
  • Implemented updateBadge() function with configurable display modes
    (none, total, today, week, all-time)
  • Modified saveToHistory() to use account-specific storage keys based on
    active email
  • Added getAccountStorageKey() helper function to generate sanitized
    storage keys
  • Changed default maxHistory from 5 to 20 aliases
  • Updated badge to trigger on history, stats, and account changes
+175/-26
App.tsx
Add multi-account support, favorites, and pagination         

entrypoints/popup/App.tsx

  • Implemented account-specific storage keys for history, stats, and
    favorites
  • Added data migration logic from old format to new account-specific
    format on first load
  • Added favorites management with toggleFavorite() function and star
    button UI
  • Implemented view mode toggle between "All" and "Favorites" with
    dedicated tabs
  • Added pagination with configurable items per page (5, 10, 20, 50)
  • Enhanced account switching to load history and favorites for selected
    account
  • Changed default maxHistory from 5 to 20 and itemsPerPage default to 10
  • Refactored random format selector from button tabs to dropdown menu
  • Removed hardcoded PRESETS array and now loads custom presets from
    settings
  • Added error handling for account email validation and duplicate
    checking
  • Improved email account form with auto-complete @gmail.com and error
    messages
+443/-173
Favorites.tsx
Refactor favorites to use account-specific storage             

entrypoints/popup/components/Favorites.tsx

  • Refactored to use account-specific storage keys for favorites
  • Changed favorites data structure from custom objects to simple email
    strings with metadata
  • Removed manual favorite creation form - favorites now added via star
    button in history
  • Updated UI to display favorites with tag badges and improved styling
  • Added listener for account-specific favorites storage changes
  • Simplified component to focus on displaying and removing favorites
+60/-132
Settings.tsx
Add account management tab and enhance settings UI             

entrypoints/popup/components/Settings.tsx

  • Added "Accounts" tab for managing email accounts with edit, delete,
    and switch functionality
  • Implemented account editing with email migration that preserves all
    data
  • Added account deletion with confirmation and data cleanup
  • Reorganized General tab into sections: Appearance & Display, Alias
    Generation, Custom Presets, Data Management, and Danger Zone
  • Added badge display setting with options (none, total, today, week,
    all-time)
  • Removed auto-save toggle setting
  • Changed maxHistory default from 5 to 20 with updated options (20-500)
  • Enhanced UI with improved styling, icons, and better visual hierarchy
  • Added helper function getAccountStorageKey() for account-specific
    storage
  • Improved form validation and error messages for account operations
+508/-183
Statistics.tsx
Implement account-specific statistics loading                       

entrypoints/popup/components/Statistics.tsx

  • Added account-specific storage key generation with
    getAccountStorageKey() helper
  • Implemented loadActiveEmailAndStats() to fetch active account and load
    its statistics
  • Updated storage change listener to detect account-specific key changes
  • Modified stats loading to use account-specific keys for history and
    stats
+44/-7   
WelcomeScreen.tsx
Add account-specific storage initialization for new users

entrypoints/popup/components/WelcomeScreen.tsx

  • Added account-specific storage initialization for new users
  • Implemented data migration from old format to account-specific format
  • Added helper function getAccountStorageKey() for generating storage
    keys
  • Initialize history, stats, and favorites keys for primary account on
    first setup
+23/-3   
Formatting
content.ts
Remove debug logging                                                                         

entrypoints/content.ts

  • Removed debug console.log statement
+0/-2     
Configuration changes
package.json
Update version to 1.1.0                                                                   

package.json

  • Bumped version from 1.0.0 to 1.1.0
+1/-1     

…n of old data format, and account-specific storage initialization
- Implement context menu creation on installation and settings change
- Load custom presets from storage for context menu options
- Update settings to remove auto-save option and rename history limit
- Bump version to 1.1.0
…or in App component; update theme selection in Settings with disabled options and informative labels
@qodo-code-review
Copy link

qodo-code-review bot commented Dec 30, 2025

PR Compliance Guide 🔍

Below is a summary of compliance checks for this PR:

Security Compliance
Sensitive data in logs

Description: The migration logic logs the active account email to the console (console.log('Migrated
old data...', activeEmail)), which can expose a user's email address in browser/extension
logs that may be collected during troubleshooting or by other local users.
App.tsx [94-110]

Referred Code
// Migrate old data format to new account-specific format if needed
if (needsMigration && (result.gmail_alias_recent || result.alias_stats || result.favorites)) {
  const historyKey = getAccountStorageKey(activeEmail, 'gmail_alias_recent');
  const statsKey = getAccountStorageKey(activeEmail, 'alias_stats');
  const favoritesKey = getAccountStorageKey(activeEmail, 'favorites');

  // Only migrate if account-specific data doesn't exist yet
  const accountData = await browser.storage.local.get([historyKey, statsKey, favoritesKey]);

  if (!accountData[historyKey] && !accountData[statsKey] && !accountData[favoritesKey]) {
    await browser.storage.local.set({
      [historyKey]: result.gmail_alias_recent || [],
      [statsKey]: result.alias_stats || { total: 0, tags: {} },
      [favoritesKey]: result.favorites || [],
    });
    console.log('Migrated old data to account-specific storage for:', activeEmail);
  }
Ticket Compliance
🎫 No ticket provided
  • Create ticket/issue
Codebase Duplication Compliance
Codebase context is not defined

Follow the guide to enable codebase context checks.

Custom Compliance
🟢
Generic: Secure Error Handling

Objective: To prevent the leakage of sensitive system information through error messages while
providing sufficient detail for internal debugging.

Status: Passed

Learn more about managing compliance generic rules or creating your own custom rules

🔴
Generic: Secure Logging Practices

Objective: To ensure logs are useful for debugging and auditing without exposing sensitive
information like PII, PHI, or cardholder data.

Status:
PII in logs: The PR adds a console.log statement that logs the user's email address during
migration, which can expose PII in logs.

Referred Code
  console.log('Migrated old data to account-specific storage for:', activeEmail);
}

Learn more about managing compliance generic rules or creating your own custom rules

Generic: Comprehensive Audit Trails

Objective: To create a detailed and reliable record of critical system actions for security analysis
and compliance.

Status:
No audit trail: Account-critical actions (e.g., account switch/delete and per-account data
deletion/migration) are executed without a structured audit log containing user
identifier, timestamp, action, and outcome.

Referred Code
const handleSwitchAccount = async (accountId: string) => {
  const updated = emailAccounts.map(acc => ({
    ...acc,
    isActive: acc.id === accountId,
  }));
  await browser.storage.local.set({ email_accounts: updated });
  const activeAccount = updated.find(acc => acc.id === accountId);
  if (activeAccount) {
    await browser.storage.local.set({ base_email: activeAccount.email });
  }
  setEmailAccounts(updated);
};

const handleDeleteAccount = async (account: EmailAccount) => {
  if (emailAccounts.length === 1) {
    alert('Cannot delete the last account. You must have at least one account.');
    return;
  }

  const confirmMsg = `Delete "${account.label}" (${account.email})?\n\nThis will permanently delete:\n• All history for this account\n• All statistics\n• All favorites\n\nThis action cannot be undone.`;



 ... (clipped 106 lines)

Learn more about managing compliance generic rules or creating your own custom rules

Generic: Meaningful Naming and Self-Documenting Code

Objective: Ensure all identifiers clearly express their purpose and intent, making code
self-documenting

Status:
Overuse of any: Multiple new code paths use any-typed variables (e.g., preset: any, acc: any, a: any)
which reduces self-documentation and makes intent and data shape unclear.

Referred Code
  customPresets.forEach((preset: any) => {
    browser.contextMenus.create({
      id: `tag-${preset.tag}`,
      parentId: "custom-tag-parent",
      title: `${preset.label} (+${preset.tag})`,
      contexts: ["editable"],
    });
  });
} else {
  // Show message if no presets
  browser.contextMenus.create({
    id: "no-presets",
    parentId: "custom-tag-parent",
    title: "No presets - Add in Settings",
    contexts: ["editable"],
    enabled: false,
  });
}

// Gmail tricks submenu
browser.contextMenus.create({


 ... (clipped 196 lines)

Learn more about managing compliance generic rules or creating your own custom rules

Generic: Robust Error Handling and Edge Case Management

Objective: Ensure comprehensive error handling that provides meaningful context and graceful
degradation

Status:
Unhandled async failures: New async storage operations for account switching/editing/deleting and data migration
lack error handling, so storage failures could leave accounts and per-account keys in a
partially migrated or inconsistent state.

Referred Code
const handleSwitchAccount = async (accountId: string) => {
  const updated = emailAccounts.map(acc => ({
    ...acc,
    isActive: acc.id === accountId,
  }));
  await browser.storage.local.set({ email_accounts: updated });
  const activeAccount = updated.find(acc => acc.id === accountId);
  if (activeAccount) {
    await browser.storage.local.set({ base_email: activeAccount.email });
  }
  setEmailAccounts(updated);
};

const handleDeleteAccount = async (account: EmailAccount) => {
  if (emailAccounts.length === 1) {
    alert('Cannot delete the last account. You must have at least one account.');
    return;
  }

  const confirmMsg = `Delete "${account.label}" (${account.email})?\n\nThis will permanently delete:\n• All history for this account\n• All statistics\n• All favorites\n\nThis action cannot be undone.`;



 ... (clipped 106 lines)

Learn more about managing compliance generic rules or creating your own custom rules

Generic: Security-First Input Validation and Data Handling

Objective: Ensure all data inputs are validated, sanitized, and handled securely to prevent
vulnerabilities

Status:
Weak email validation: New account creation validates emails only by checking for @, which may allow malformed
input that later drives account selection and account-specific storage key generation.

Referred Code
const handleAddAccount = async () => {
  setAddAccountError('');

  if (!newAccountEmail.trim()) {
    setAddAccountError('Email is required');
    return;
  }

  if (!newAccountEmail.includes('@')) {
    setAddAccountError('Please enter a valid email address');
    return;
  }

  // Check if email already exists
  const emailExists = emailAccounts.some(acc => acc.email.toLowerCase() === newAccountEmail.trim().toLowerCase());
  if (emailExists) {
    setAddAccountError('This email address is already added!');
    return;
  }

  const newAccount = {


 ... (clipped 19 lines)

Learn more about managing compliance generic rules or creating your own custom rules

  • Update
Compliance status legend 🟢 - Fully Compliant
🟡 - Partial Compliant
🔴 - Not Compliant
⚪ - Requires Further Human Verification
🏷️ - Compliance label

@qodo-code-review
Copy link

qodo-code-review bot commented Dec 30, 2025

PR Code Suggestions ✨

Explore these optional code suggestions:

CategorySuggestion                                                                                                                                    Impact
Possible issue
Update alias emails during migration

Update the email addresses within the history and favorites arrays during
account email migration to reflect the new email's domain.

entrypoints/popup/components/Settings.tsx [206-274]

 const handleSaveEdit = async (accountId: string) => {
   if (!editingLabel.trim()) {
     alert('Label cannot be empty');
     return;
   }
 
   if (!editingEmail.trim() || !editingEmail.includes('@')) {
     alert('Please enter a valid email address');
     return;
   }
 
   const account = emailAccounts.find(acc => acc.id === accountId);
   if (!account) return;
 
   const oldEmail = account.email;
   const newEmail = editingEmail.trim();
 
   // Check if email changed
   if (oldEmail !== newEmail) {
     // Check if new email already exists in another account
     const emailExists = emailAccounts.some(acc => acc.id !== accountId && acc.email === newEmail);
     if (emailExists) {
       alert('This email address is already used by another account!');
       return;
     }
 
     const confirmMsg = `Change email from\n${oldEmail}\nto\n${newEmail}?\n\nThis will:\n• Migrate all history, statistics, and favorites to the new email\n• Update the account email\n• Delete data associated with the old email\n\nContinue?`;
     
     if (!confirm(confirmMsg)) return;
 
     // Migrate data from old email to new email
     const oldHistoryKey = getAccountStorageKey(oldEmail, 'gmail_alias_recent');
     const oldStatsKey = getAccountStorageKey(oldEmail, 'alias_stats');
     const oldFavoritesKey = getAccountStorageKey(oldEmail, 'favorites');
 
     const newHistoryKey = getAccountStorageKey(newEmail, 'gmail_alias_recent');
     const newStatsKey = getAccountStorageKey(newEmail, 'alias_stats');
     const newFavoritesKey = getAccountStorageKey(newEmail, 'favorites');
 
     // Get old data
     const oldData = await browser.storage.local.get([oldHistoryKey, oldStatsKey, oldFavoritesKey]);
+    const [, newDomain] = newEmail.split('@');
 
-    // Save to new keys
+    // Update email addresses in history
+    const migratedHistory = (oldData[oldHistoryKey] || []).map((alias: any) => ({
+      ...alias,
+      email: alias.email.replace(/@.+/, `@${newDomain}`),
+    }));
+
+    // Update email addresses in favorites
+    const migratedFavorites = (oldData[oldFavoritesKey] || []).map((fav: any) => ({
+      ...fav,
+      email: fav.email.replace(/@.+/, `@${newDomain}`),
+    }));
+
+    // Save to new keys with updated emails
     await browser.storage.local.set({
-      [newHistoryKey]: oldData[oldHistoryKey] || [],
+      [newHistoryKey]: migratedHistory,
       [newStatsKey]: oldData[oldStatsKey] || { total: 0, tags: {} },
-      [newFavoritesKey]: oldData[oldFavoritesKey] || [],
+      [newFavoritesKey]: migratedFavorites,
     });
 
     // Delete old keys
     await browser.storage.local.remove([oldHistoryKey, oldStatsKey, oldFavoritesKey]);
 
     // Update base_email if this is the active account
     if (account.isActive) {
       await browser.storage.local.set({ base_email: newEmail });
     }
   }
 
   // Update account in list
   const updated = emailAccounts.map(acc => 
     acc.id === accountId ? { ...acc, label: editingLabel.trim(), email: editingEmail.trim() } : acc
   );
   
   await browser.storage.local.set({ email_accounts: updated });
   setEmailAccounts(updated);
   setEditingAccountId(null);
   setEditingLabel('');
   setEditingEmail('');
 };
  • Apply / Chat
Suggestion importance[1-10]: 9

__

Why: This suggestion identifies a critical bug in the data migration logic where alias emails in history and favorites are not updated to the new domain, leading to incorrect data.

High
Fix stale state in listener

Refactor the storage change listener to combine separate get calls for history
and favorites into a single, more efficient call when the active account
changes.

entrypoints/popup/App.tsx [151-207]

 // Listen for settings changes
 useEffect(() => {
   const handleStorageChange = async (changes: any) => {
     if (changes.app_settings) {
       const newSettings = changes.app_settings.newValue;
       if (newSettings) {
         setMaxRecent(newSettings.maxHistory || 20);
         setCustomPresets(newSettings.customPresets || []);
         setRandomFormat(newSettings.randomFormat || 'private-mail');
       }
     }
     if (changes.email_accounts) {
       const newAccounts = changes.email_accounts.newValue;
       if (newAccounts) {
         setEmailAccounts(newAccounts);
         setHasEmailAccounts(newAccounts.length > 0);
         // Update base email if active account changed
         const activeAccount = newAccounts.find((acc: any) => acc.isActive);
         if (activeAccount && activeAccount.email !== baseEmail) {
-          setBaseEmail(activeAccount.email);
-          // Load history for new account
-          const historyKey = getAccountStorageKey(activeAccount.email, 'gmail_alias_recent');
-          const historyResult = await browser.storage.local.get(historyKey);
-          if (historyResult[historyKey] && Array.isArray(historyResult[historyKey])) {
-            setRecentAliases(historyResult[historyKey] as Alias[]);
-          } else {
-            setRecentAliases([]);
-          }
-          // Load favorites for new account
-          const favoritesKey = getAccountStorageKey(activeAccount.email, 'favorites');
-          const favResult = await browser.storage.local.get(favoritesKey);
-          if (favResult[favoritesKey] && Array.isArray(favResult[favoritesKey])) {
-            const favEmails = favResult[favoritesKey].map((f: any) => f.email);
-            setFavorites(favEmails);
-          } else {
-            setFavorites([]);
-          }
+          const newActiveEmail = activeAccount.email;
+          setBaseEmail(newActiveEmail);
+
+          // Load history and favorites for the new account
+          const historyKey = getAccountStorageKey(newActiveEmail, 'gmail_alias_recent');
+          const favoritesKey = getAccountStorageKey(newActiveEmail, 'favorites');
+          const result = await browser.storage.local.get([historyKey, favoritesKey]);
+
+          setRecentAliases((result[historyKey] as Alias[]) || []);
+
+          const favs = result[favoritesKey] || [];
+          const favEmails = Array.isArray(favs) ? favs.map((f: any) => f.email) : [];
+          setFavorites(favEmails);
         }
       }
     }
     
-    // Listen for favorites changes
+    // Listen for favorites changes for the current account
     const favoritesKey = getAccountStorageKey(baseEmail, 'favorites');
     if (changes[favoritesKey]) {
       const newFavorites = changes[favoritesKey].newValue;
       if (newFavorites && Array.isArray(newFavorites)) {
         const favEmails = newFavorites.map((f: any) => f.email);
         setFavorites(favEmails);
       } else {
         setFavorites([]);
       }
     }
   };
 
   browser.storage.onChanged.addListener(handleStorageChange);
   return () => browser.storage.onChanged.removeListener(handleStorageChange);
 }, [baseEmail]);
  • Apply / Chat
Suggestion importance[1-10]: 4

__

Why: The suggestion correctly identifies an opportunity to improve performance by combining two storage get calls into one. While the original code is functionally correct, the proposed change is more efficient.

Low
General
Lowercase email in storage keys

In getAccountStorageKey, convert the email to lowercase before sanitization to
ensure consistent storage keys.

entrypoints/background.ts [296-299]

 function getAccountStorageKey(email: string, suffix: string): string {
-  const sanitized = email.replace(/[^a-zA-Z0-9]/g, "_");
+  const sanitized = email.toLowerCase().replace(/[^a-z0-9]/g, "_");
   return `${suffix}_${sanitized}`;
 }
  • Apply / Chat
Suggestion importance[1-10]: 7

__

Why: This is a good suggestion for data consistency. Normalizing emails to lowercase prevents creating duplicate storage entries for the same email address with different casing, improving robustness.

Medium
Improve active account detection logic

In loadActiveEmailAndStats, add a check to ensure email_accounts is not empty
before trying to find an active account to prevent using a stale base_email.

entrypoints/popup/components/Statistics.tsx [51-73]

 const loadActiveEmailAndStats = async () => {
   // Get active account first
   const accountResult = await browser.storage.local.get(['email_accounts', 'base_email']);
   let email = 'your.email@gmail.com';
   
-  if (accountResult.email_accounts && Array.isArray(accountResult.email_accounts)) {
+  if (accountResult.email_accounts && Array.isArray(accountResult.email_accounts) && accountResult.email_accounts.length > 0) {
     const activeAccount = accountResult.email_accounts.find((acc: any) => acc.isActive);
     if (activeAccount) {
       email = activeAccount.email;
+    } else {
+      // Fallback to the first account if none are active
+      email = accountResult.email_accounts[0].email;
     }
   } else if (accountResult.base_email) {
     email = accountResult.base_email;
   }
   
   setActiveEmail(email);
   
   // Load stats for this account
   const historyKey = getAccountStorageKey(email, 'gmail_alias_recent');
   const statsKey = getAccountStorageKey(email, 'alias_stats');
   
   const result = await browser.storage.local.get([historyKey, statsKey]);
   const recent = (result[historyKey] || []) as any[];
   const savedStats = (result[statsKey] || { total: 0, tags: {} }) as { total: number; tags: Record<string, number> };
 ...

[To ensure code accuracy, apply this suggestion manually]

Suggestion importance[1-10]: 6

__

Why: The suggestion correctly points out a potential edge case where email_accounts is empty, and the code could fall back to a stale base_email. The proposed change makes the active account detection more robust.

Low
High-level
Consolidate duplicated helper functions

The getAccountStorageKey function is repeated in multiple files. It should be
moved to a shared utility file and imported where needed to avoid code
duplication and improve maintainability.

Examples:

entrypoints/background.ts [296-299]
  function getAccountStorageKey(email: string, suffix: string): string {
    const sanitized = email.replace(/[^a-zA-Z0-9]/g, "_");
    return `${suffix}_${sanitized}`;
  }
entrypoints/popup/App.tsx [41-44]
const getAccountStorageKey = (email: string, suffix: string) => {
  const sanitized = email.replace(/[^a-zA-Z0-9]/g, '_');
  return `${suffix}_${sanitized}`;
};

Solution Walkthrough:

Before:

// In entrypoints/popup/App.tsx
const getAccountStorageKey = (email: string, suffix: string) => {
  const sanitized = email.replace(/[^a-zA-Z0-9]/g, '_');
  return `${suffix}_${sanitized}`;
};

function App() {
  // ... uses getAccountStorageKey
}

// In entrypoints/background.ts
function getAccountStorageKey(email: string, suffix: string): string {
  const sanitized = email.replace(/[^a-zA-Z0-9]/g, "_");
  return `${suffix}_${sanitized}`;
}
// ... and so on in other files

After:

// In a new file, e.g., 'utils/storage.ts'
export const getAccountStorageKey = (email: string, suffix: string) => {
  const sanitized = email.replace(/[^a-zA-Z0-9]/g, '_');
  return `${suffix}_${sanitized}`;
};

// In entrypoints/popup/App.tsx
import { getAccountStorageKey } from '../utils/storage';

function App() {
  // ... uses imported getAccountStorageKey
}

// In entrypoints/background.ts
import { getAccountStorageKey } from '../utils/storage';
// ... uses imported getAccountStorageKey
Suggestion importance[1-10]: 6

__

Why: The suggestion correctly identifies that the getAccountStorageKey function is duplicated across multiple files, which is a valid maintainability concern for a core piece of logic introduced in this PR.

Low
  • Update

@hoangsvit hoangsvit merged commit e3bafc5 into main Dec 30, 2025
4 checks passed
@github-actions
Copy link

Thanks for helping make Gmail Alias Toolkit better!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants