ultisuite-client/lib/mail-folder-filter.ts
2026-05-25 13:52:40 +02:00

202 lines
6.0 KiB
TypeScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import type { Email } from "@/lib/email-data"
import {
DEFAULT_INBOX_TAB,
INBOX_ALL_TAB,
normalizeInboxTabSegment,
} from "@/lib/mail-url"
import {
folderTree as defaultFolderTree,
sidebarNavFolderIdToLabel as defaultSidebarNavFolderIdToLabel,
defaultNavLabelRowsSnapshot,
type LabelRowItem,
} from "@/lib/sidebar-nav-data"
import { collectSubtreeFolderIds } from "@/lib/sidebar-nav-maps"
import type { FolderTreeNode } from "@/lib/sidebar-nav-maps"
export type MailFolderFilterCtx = {
starredEmailIds: string[]
importantEmailIds: string[]
}
/** Carte navigation dynamique (sidebar). Si absent, valeurs par défaut du module. */
export type MailNavFolderMaps = {
folderIdToLabel: Record<string, string>
folderTree: FolderTreeNode[]
labelRows: LabelRowItem[]
}
function hasFutureScheduledSend(email: Email): boolean {
if (!email.scheduledSendAt) return false
const t = new Date(email.scheduledSendAt).getTime()
if (!Number.isFinite(t)) return false
return t > Date.now()
}
function effectiveStarred(email: Email, ctx: MailFolderFilterCtx): boolean {
return ctx.starredEmailIds.includes(email.id) || email.starred
}
function effectiveImportant(email: Email, ctx: MailFolderFilterCtx): boolean {
return ctx.importantEmailIds.includes(email.id) || email.important
}
function isInInbox(email: Email): boolean {
if (email.deleted) return false
if (email.spam) return false
if (hasFutureScheduledSend(email)) return false
const ls = email.labels
if (!ls?.length) return true
return ls.includes("inbox")
}
function labelRowForNavId(
folderId: string,
rows: readonly LabelRowItem[]
): LabelRowItem | undefined {
return rows.find((r) => r.id === folderId)
}
function resolveNavMaps(maps?: MailNavFolderMaps | null): MailNavFolderMaps {
if (maps) return maps
return {
folderIdToLabel: defaultSidebarNavFolderIdToLabel as Record<string, string>,
folderTree: defaultFolderTree,
labelRows: defaultNavLabelRowsSnapshot,
}
}
function emailHasAnyLabel(email: Email, labels: string[]): boolean {
const ls = email.labels
if (!ls?.length) return false
const lower = new Set(ls.map((x) => x.toLowerCase()))
return labels.some((lab) => lower.has(lab.toLowerCase()))
}
function matchesFolderLabelRow(
email: Email,
folderId: string,
maps: MailNavFolderMaps
): boolean {
const row = labelRowForNavId(folderId, maps.labelRows)
if (row && row.enabled === false) return false
const label = maps.folderIdToLabel[folderId]
if (!label) return false
return emailHasAnyLabel(email, [label])
}
/** Dossier hiérarchique : ce nœud ou n'importe quel sous-dossier (décompte unique par mail). */
function matchesLabelNav(
email: Email,
folderId: string,
maps: MailNavFolderMaps,
subtreeIdsCache?: Map<string, string[] | null>
): boolean {
let subtreeIds: string[] | null
if (subtreeIdsCache) {
if (!subtreeIdsCache.has(folderId)) {
subtreeIdsCache.set(
folderId,
collectSubtreeFolderIds(maps.folderTree, folderId)
)
}
subtreeIds = subtreeIdsCache.get(folderId) ?? null
} else {
subtreeIds = collectSubtreeFolderIds(maps.folderTree, folderId)
}
if (subtreeIds) {
return subtreeIds.some((id) => matchesFolderLabelRow(email, id, maps))
}
return matchesFolderLabelRow(email, folderId, maps)
}
/** Onglet Principale : boîte sans libellés système « exclude » actifs. */
export function emailMatchesInboxPrimaryTab(
email: Email,
ctx: MailFolderFilterCtx,
maps: MailNavFolderMaps,
subtreeIdsCache?: Map<string, string[] | null>
): boolean {
if (!emailMatchesFolder(email, "inbox", ctx, maps, subtreeIdsCache)) return false
if (email.deleted) return false
if (hasFutureScheduledSend(email)) return false
for (const row of maps.labelRows) {
if (row.enabled === false) continue
if (!row.excludeFromPrincipal) continue
if (emailHasAnyLabel(email, [row.label])) return false
}
return true
}
/** Onglet boîte de réception (Principale, catégorie, Tous les messages). */
export function emailMatchesInboxTab(
email: Email,
tabId: string,
ctx: MailFolderFilterCtx,
maps: MailNavFolderMaps,
subtreeIdsCache?: Map<string, string[] | null>
): boolean {
const tab = normalizeInboxTabSegment(tabId)
if (tab === INBOX_ALL_TAB) {
return emailMatchesFolder(email, "inbox", ctx, maps, subtreeIdsCache)
}
if (tab === DEFAULT_INBOX_TAB) {
return emailMatchesInboxPrimaryTab(email, ctx, maps, subtreeIdsCache)
}
return (
emailMatchesFolder(email, "inbox", ctx, maps, subtreeIdsCache) &&
emailMatchesFolder(email, tab, ctx, maps, subtreeIdsCache)
)
}
export function emailMatchesFolder(
email: Email,
folderId: string,
ctx: MailFolderFilterCtx,
maps?: MailNavFolderMaps | null,
/** Réutiliser entre appels (ex. `computeFolderUnreadCounts`) pour éviter de rescanner larbre à chaque mail. */
subtreeIdsCache?: Map<string, string[] | null>
): boolean {
const nav = resolveNavMaps(maps)
if (email.deleted && folderId !== "trash") {
return false
}
switch (folderId) {
case "inbox":
return isInInbox(email)
case "starred":
return effectiveStarred(email, ctx)
case "snoozed":
return (email.labels?.includes("snoozed") ?? false) && !email.deleted
case "important":
return effectiveImportant(email, ctx)
case "sent":
return (email.labels?.includes("sent") ?? false) && !email.deleted
case "drafts":
return (email.labels?.includes("drafts") ?? false) && !email.deleted
case "scheduled":
return !email.deleted && hasFutureScheduledSend(email)
case "spam":
return (
!email.deleted &&
(email.spam === true || (email.labels?.includes("spam") ?? false))
)
case "trash":
return email.deleted === true
default:
break
}
const row = labelRowForNavId(folderId, nav.labelRows)
if (row && row.enabled === false) return false
if (nav.folderIdToLabel[folderId]) {
return matchesLabelNav(email, folderId, nav, subtreeIdsCache)
}
if (email.labels?.includes(folderId)) return true
return false
}