Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 4 additions & 34 deletions entrypoints/content/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import {
} from "src/utils/inlineSiteSettings";
import { t } from "../../lib/i18n";
import { USER_GUIDE_URL } from "src/utils/externalLinks";
import { loadInlineAccountHistory } from "src/services/inlineHistoryService";
import {
getPreviousAliasForWebsite,
generateSuggestionsForWebsite,
Expand Down Expand Up @@ -710,40 +711,9 @@ function createPopup(
if (!historyList) return;

try {
// Use the same account that was used to generate this popup's aliases.
const historyKey = getAccountStorageKey(
data.activeEmail,
"gmail_alias_recent",
);
const favoritesKey = getAccountStorageKey(data.activeEmail, "favorites");

const storage = (await browser.storage.local.get([
historyKey,
favoritesKey,
"gmail_alias_recent",
"favorites",
])) as Record<string, unknown>;
// Older installations may still have history under the global key.
const history = (storage[historyKey] ??
storage.gmail_alias_recent ??
[]) as Array<{
email: string;
timestamp: number;
}>;
currentHistory = history
.filter((item) => item && typeof item.email === "string")
.slice()
.sort((a, b) => b.timestamp - a.timestamp);
const favorites = (storage[favoritesKey] ?? storage.favorites ?? []) as
| Array<{ email?: string } | string>
| undefined;
currentFavorites = Array.isArray(favorites)
? favorites
.map((favorite) =>
typeof favorite === "string" ? favorite : favorite.email,
)
.filter((email): email is string => Boolean(email))
: [];
const accountHistory = await loadInlineAccountHistory(data.activeEmail);
currentHistory = accountHistory.history;
currentFavorites = accountHistory.favorites;

if (historyTag) {
const selectedTag = historyTag.value;
Expand Down
104 changes: 104 additions & 0 deletions src/services/inlineHistoryService.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
import {
filterAliasesForAccount,
getAccountStorageKey,
} from "../../entrypoints/popup/utils";

export interface InlineHistoryAlias {
email: string;
timestamp: number;
}

export interface InlineAccountHistory {
history: InlineHistoryAlias[];
favorites: string[];
}

interface FavoriteAlias {
email: string;
}

/** Returns whether a stored value is a usable inline history entry. */
function isInlineHistoryAlias(value: unknown): value is InlineHistoryAlias {
if (!value || typeof value !== "object") return false;
const candidate = value as { email?: unknown; timestamp?: unknown };
return (
typeof candidate.email === "string" &&
typeof candidate.timestamp === "number"
);
}

/** Normalizes legacy string and object favorite values into one shape. */
function normalizeFavoriteAlias(value: unknown): FavoriteAlias | null {
if (typeof value === "string" && value) return { email: value };
if (!value || typeof value !== "object") return null;

const email = (value as { email?: unknown }).email;
return typeof email === "string" && email ? { email } : null;
}

/** Sorts history entries from newest to oldest. */
function sortHistoryNewestFirst(
first: InlineHistoryAlias,
second: InlineHistoryAlias,
): number {
return second.timestamp - first.timestamp;
}

/** Extracts valid history entries from account-scoped or legacy storage. */
function parseHistory(value: unknown): InlineHistoryAlias[] {
if (!Array.isArray(value)) return [];

const history: InlineHistoryAlias[] = [];
for (const entry of value) {
if (isInlineHistoryAlias(entry)) history.push(entry);
}
return history;
}

/** Extracts valid favorite aliases from legacy string and object formats. */
function parseFavorites(value: unknown): FavoriteAlias[] {
if (!Array.isArray(value)) return [];

const favorites: FavoriteAlias[] = [];
for (const entry of value) {
const favorite = normalizeFavoriteAlias(entry);
if (favorite) favorites.push(favorite);
}
return favorites;
}

/** Returns the first available account-scoped value, then its legacy fallback. */
function selectStoredValue(
storage: Record<string, unknown>,
accountKey: string,
legacyKey: string,
): unknown {
return storage[accountKey] ?? storage[legacyKey] ?? [];
}

/** Loads account-isolated history and favorites for the inline helper popup. */
export async function loadInlineAccountHistory(
activeEmail: string,
): Promise<InlineAccountHistory> {
const historyKey = getAccountStorageKey(activeEmail, "gmail_alias_recent");
const favoritesKey = getAccountStorageKey(activeEmail, "favorites");
const storage = (await browser.storage.local.get([
historyKey,
favoritesKey,
"gmail_alias_recent",
"favorites",
])) as Record<string, unknown>;

const history = filterAliasesForAccount(
parseHistory(selectStoredValue(storage, historyKey, "gmail_alias_recent")),
activeEmail,
).sort(sortHistoryNewestFirst);
const favoriteAliases = filterAliasesForAccount(
parseFavorites(selectStoredValue(storage, favoritesKey, "favorites")),
activeEmail,
);
const favorites: string[] = [];
for (const favorite of favoriteAliases) favorites.push(favorite.email);

return { history, favorites };
}
44 changes: 25 additions & 19 deletions src/services/websiteAliasService.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
import { normalizeHostname } from "../utils/hostnameNormalizer";
import { getAccountStorageKey } from "../../entrypoints/popup/utils";
import {
generateAlias,
getAccountStorageKey,
isAliasForAccount,
validateEmail,
} from "../../entrypoints/popup/utils";

interface WebsiteAliasEntry {
alias: string;
Expand Down Expand Up @@ -62,7 +67,8 @@ export async function getPreviousAliasForWebsite(
const map = await getWebsiteAliasMap(email);
const entry = map[normalized];

return entry ? { alias: entry.alias, timestamp: entry.timestamp } : null;
if (!entry || !isAliasForAccount(entry.alias, email)) return null;
return { alias: entry.alias, timestamp: entry.timestamp };
}

/**
Expand All @@ -74,36 +80,36 @@ export async function generateSuggestionsForWebsite(
urlOrHostname: string,
): Promise<string[]> {
const normalized = normalizeHostname(urlOrHostname);
if (!normalized) return [];

// Extract base email part (before @gmail.com)
const emailBase = baseEmail.split("@")[0];
if (!normalized || !validateEmail(baseEmail).isValid) return [];

const suggestions: string[] = [];

// 1. Simple: base+normalized
suggestions.push(`${emailBase}+${normalized}@gmail.com`);
/** Adds a valid plus-addressed alias while preserving the active account domain. */
function appendSuggestion(tag: string): void {
const alias = generateAlias(baseEmail, tag);
if (alias) suggestions.push(alias);
}

// 1. Simple: base+normalized@configured-domain
appendSuggestion(normalized);

// 2. Counter: base+normalized001
// 2. Counter: base+normalized001@configured-domain
const map = await getWebsiteAliasMap(baseEmail);
const count = map[normalized]?.generatedCount || 0;
if (count > 0) {
suggestions.push(
`${emailBase}+${normalized}${String(count + 1).padStart(3, "0")}@gmail.com`,
);
appendSuggestion(`${normalized}${String(count + 1).padStart(3, "0")}`);
}

// 3. Short: base+first-3-letters
const shortCode = normalized.slice(0, 3);
suggestions.push(`${emailBase}+${shortCode}@gmail.com`);
// 3. Short: base+first-3-letters@configured-domain
appendSuggestion(normalized.slice(0, 3));

// 4. With date: base+normalized-YYYYMMDD
// 4. With date: base+normalized-YYYYMMDD@configured-domain
const today = new Date().toISOString().slice(0, 10).replace(/-/g, "");
suggestions.push(`${emailBase}+${normalized}-${today}@gmail.com`);
appendSuggestion(`${normalized}-${today}`);

// 5. Random suffix: base+normalized-XXXX
// 5. Random suffix: base+normalized-XXXX@configured-domain
const randomSuffix = Math.random().toString(36).substring(2, 6);
suggestions.push(`${emailBase}+${normalized}-${randomSuffix}@gmail.com`);
appendSuggestion(`${normalized}-${randomSuffix}`);

return suggestions.slice(0, 5);
}
Expand Down
Loading