feat: add contacts phase 2, advanced search, vacation responder, Docker & TOTP 2FA
- Contact groups/lists, vCard import/export (RFC 6350), bulk operations - Advanced search with JMAP filter panel, search chips, cross-mailbox queries - Vacation responder with JMAP VacationResponse, settings tab, sidebar indicator - TOTP two-factor authentication support - Docker multi-stage build with standalone output and docker-compose - CSP Report-Only headers and security headers via proxy middleware - Virtual scrolling for large email lists - Structured server-side logger (text/JSON, configurable level) - 450+ tests (contacts, vCard, threads, headers, identity, components) - Playwright E2E framework setup - Updated README and ROADMAP with all new features
This commit is contained in:
@@ -0,0 +1,129 @@
|
||||
export interface SearchFilters {
|
||||
from: string;
|
||||
to: string;
|
||||
subject: string;
|
||||
body: string;
|
||||
hasAttachment: boolean | null;
|
||||
dateAfter: string;
|
||||
dateBefore: string;
|
||||
isUnread: boolean | null;
|
||||
isStarred: boolean | null;
|
||||
}
|
||||
|
||||
export const DEFAULT_SEARCH_FILTERS: SearchFilters = {
|
||||
from: "",
|
||||
to: "",
|
||||
subject: "",
|
||||
body: "",
|
||||
hasAttachment: null,
|
||||
dateAfter: "",
|
||||
dateBefore: "",
|
||||
isUnread: null,
|
||||
isStarred: null,
|
||||
};
|
||||
|
||||
export function buildJMAPFilter(
|
||||
textQuery: string,
|
||||
filters: SearchFilters,
|
||||
mailboxId?: string
|
||||
): Record<string, unknown> {
|
||||
const conditions: Record<string, unknown>[] = [];
|
||||
|
||||
if (textQuery) {
|
||||
conditions.push({ text: textQuery });
|
||||
}
|
||||
|
||||
if (filters.from) {
|
||||
conditions.push({ from: filters.from });
|
||||
}
|
||||
|
||||
if (filters.to) {
|
||||
conditions.push({ to: filters.to });
|
||||
}
|
||||
|
||||
if (filters.subject) {
|
||||
conditions.push({ subject: filters.subject });
|
||||
}
|
||||
|
||||
if (filters.body) {
|
||||
conditions.push({ body: filters.body });
|
||||
}
|
||||
|
||||
if (filters.hasAttachment === true) {
|
||||
conditions.push({ hasAttachment: true });
|
||||
} else if (filters.hasAttachment === false) {
|
||||
conditions.push({ hasAttachment: false });
|
||||
}
|
||||
|
||||
if (filters.dateAfter) {
|
||||
const date = new Date(filters.dateAfter);
|
||||
if (!isNaN(date.getTime())) {
|
||||
conditions.push({ after: date.toISOString() });
|
||||
}
|
||||
}
|
||||
|
||||
if (filters.dateBefore) {
|
||||
const endOfDay = new Date(filters.dateBefore);
|
||||
if (!isNaN(endOfDay.getTime())) {
|
||||
endOfDay.setHours(23, 59, 59, 999);
|
||||
conditions.push({ before: endOfDay.toISOString() });
|
||||
}
|
||||
}
|
||||
|
||||
if (filters.isUnread === true) {
|
||||
conditions.push({ notKeyword: "$seen" });
|
||||
} else if (filters.isUnread === false) {
|
||||
conditions.push({ hasKeyword: "$seen" });
|
||||
}
|
||||
|
||||
if (filters.isStarred === true) {
|
||||
conditions.push({ hasKeyword: "$flagged" });
|
||||
} else if (filters.isStarred === false) {
|
||||
conditions.push({ notKeyword: "$flagged" });
|
||||
}
|
||||
|
||||
if (mailboxId) {
|
||||
conditions.push({ inMailbox: mailboxId });
|
||||
}
|
||||
|
||||
if (conditions.length === 0) {
|
||||
return mailboxId ? { inMailbox: mailboxId } : {};
|
||||
}
|
||||
|
||||
if (conditions.length === 1) {
|
||||
return conditions[0];
|
||||
}
|
||||
|
||||
return {
|
||||
operator: "AND",
|
||||
conditions,
|
||||
};
|
||||
}
|
||||
|
||||
export function isFilterEmpty(filters: SearchFilters): boolean {
|
||||
return (
|
||||
!filters.from &&
|
||||
!filters.to &&
|
||||
!filters.subject &&
|
||||
!filters.body &&
|
||||
filters.hasAttachment === null &&
|
||||
!filters.dateAfter &&
|
||||
!filters.dateBefore &&
|
||||
filters.isUnread === null &&
|
||||
filters.isStarred === null
|
||||
);
|
||||
}
|
||||
|
||||
export function activeFilterCount(filters: SearchFilters): number {
|
||||
let count = 0;
|
||||
if (filters.from) count++;
|
||||
if (filters.to) count++;
|
||||
if (filters.subject) count++;
|
||||
if (filters.body) count++;
|
||||
if (filters.hasAttachment !== null) count++;
|
||||
if (filters.dateAfter) count++;
|
||||
if (filters.dateBefore) count++;
|
||||
if (filters.isUnread !== null) count++;
|
||||
if (filters.isStarred !== null) count++;
|
||||
return count;
|
||||
}
|
||||
Reference in New Issue
Block a user