From fb8986de677bea7ca5131cb1c2a838efee3fcf0d Mon Sep 17 00:00:00 2001 From: Rupraj Singh Date: Sat, 18 Jul 2026 11:27:29 +0530 Subject: [PATCH 1/3] fixed ESLINT errors --- lint.md | 81 ++++++ src/api/admin-domain.ts | 18 +- src/api/auth.ts | 6 +- src/api/config.ts | 2 +- src/api/contacts.ts | 14 +- src/api/fetchWrapper.ts | 7 +- src/api/mailbox.ts | 29 +-- src/api/search.ts | 13 +- src/api/sieve.ts | 10 +- .../admin/domain/BrandingManager.tsx | 5 +- src/components/admin/domain/BulkEdit.tsx | 12 +- src/components/admin/domain/BulkImport.tsx | 33 +-- .../admin/domain/DomainDetailsModal.tsx | 2 +- src/components/admin/domain/DomainForm.tsx | 15 +- src/components/admin/domain/Login.tsx | 2 +- src/components/admin/domain/index.tsx | 20 +- src/components/common/BimiAvatar.tsx | 2 +- src/components/common/DropdownWrapper.tsx | 6 +- src/components/common/LeftPanel.tsx | 16 +- .../common/MinimizedModalContext.tsx | 24 +- src/components/common/NestedFolderSelect.tsx | 5 +- src/components/common/Pagination.tsx | 2 +- src/components/common/TourTips.tsx | 9 +- .../common/header/LayoutSetting.tsx | 7 +- src/components/common/header/Profile.tsx | 25 +- src/components/common/header/index.tsx | 37 ++- .../common/header/search/AdvancedFilters.tsx | 9 +- .../common/header/search/FolderSelect.tsx | 7 +- src/components/common/header/search/index.tsx | 22 +- src/components/common/header/search/types.ts | 4 +- .../common/minimizedModalsContext.ts | 33 +++ .../composer/AttachmentUploader.tsx | 51 ++-- src/components/composer/CustomModal.tsx | 4 +- .../composer/EmailPriorityField.tsx | 3 +- src/components/composer/FolderSelectField.tsx | 18 +- src/components/composer/RecipientField.tsx | 6 +- src/components/composer/SubjectField.tsx | 1 - src/components/composer/TempelateSelector.tsx | 2 +- src/components/composer/attachmentUtils.ts | 40 +++ .../composer/contentEditor/AdvancedMenu.tsx | 6 +- .../composer/contentEditor/InfoTooltip.tsx | 2 +- .../contentEditor/SlashCommandList.tsx | 157 ++++++------ .../composer/contentEditor/SlashCommands.ts | 32 +-- .../composer/contentEditor/index.tsx | 16 +- src/components/composer/index.tsx | 107 ++++---- src/components/contacts/BulkCreateView.tsx | 8 +- src/components/contacts/DeleteConfirm.tsx | 2 +- src/components/contacts/index.tsx | 34 +-- src/components/help/index.tsx | 6 +- src/components/login/Login.tsx | 6 +- src/components/login/LoginNoRecaptcha.tsx | 5 +- src/components/mailbox/ComposeButton.tsx | 10 - src/components/mailbox/CreateFolder.tsx | 4 +- src/components/mailbox/CustomFolder.tsx | 13 +- src/components/mailbox/DefaultFolder.tsx | 6 +- src/components/mailbox/EmailActions.tsx | 64 ++--- src/components/mailbox/EmailAttachments.tsx | 231 ++---------------- src/components/mailbox/EmailCard.tsx | 24 +- src/components/mailbox/EmailComposer.tsx | 191 ++++++++++----- src/components/mailbox/EmailEmptyState.tsx | 13 +- src/components/mailbox/EmailHoverCard.tsx | 9 +- src/components/mailbox/EmailHtmlContent.tsx | 9 +- src/components/mailbox/EmailList.tsx | 212 +++++++++------- src/components/mailbox/EmailTabs.tsx | 56 +++-- src/components/mailbox/EmailTextContent.tsx | 8 +- src/components/mailbox/EmailToolbar.tsx | 24 +- src/components/mailbox/EmailViewer.tsx | 63 ++--- src/components/mailbox/FolderItem.tsx | 3 +- src/components/mailbox/FolderRow.tsx | 12 +- src/components/mailbox/Folders.tsx | 45 ++-- src/components/mailbox/ICSViewer.tsx | 37 ++- src/components/mailbox/MobileBottomNav.tsx | 2 +- src/components/mailbox/MoveEmail.tsx | 16 +- src/components/mailbox/RecipientSection.tsx | 2 +- src/components/mailbox/SingleEmailView.tsx | 27 +- src/components/mailbox/ThreadEmailCard.tsx | 104 ++++---- src/components/mailbox/ThreadView.tsx | 50 ++-- .../mailbox/composeButtonOptions.ts | 28 +++ src/components/mailbox/folderSchema.ts | 30 +++ src/components/mailbox/index.tsx | 21 +- .../settings/filters/CreateScriptDialoge.tsx | 6 +- .../settings/filters/FiltersList.tsx | 3 +- .../settings/filters/RenderFields.tsx | 34 +-- .../settings/filters/filterTransform.ts | 78 ++++-- src/components/settings/filters/index.tsx | 84 ++++--- src/components/settings/filters/validation.ts | 17 +- .../settings/folders/ACLManager.tsx | 11 +- .../settings/folders/FolderStats.tsx | 2 +- .../settings/folders/FolderTable.tsx | 2 +- src/components/settings/folders/index.tsx | 31 +-- .../settings/general/SignatureSelector.tsx | 1 - src/components/settings/general/index.tsx | 32 +-- src/components/settings/mapping/index.tsx | 6 +- .../settings/tempelates/TempelateCard.tsx | 2 - .../settings/tempelates/TempelateModal.tsx | 2 +- src/components/settings/tempelates/index.tsx | 4 +- src/components/settings/vacation/index.tsx | 14 +- .../settings/vacation/vacation-transform.ts | 4 +- src/components/ui/DevReload.tsx | 2 +- src/components/ui/ErrorBoundary.tsx | 2 +- src/components/ui/InputComponents.tsx | 8 +- src/components/ui/ThemeWrapper.tsx | 3 +- src/components/ui/ToastComponent.tsx | 47 +--- src/components/ui/toastContext.ts | 51 ++++ src/hooks/useAdminDomain.ts | 13 +- src/hooks/useEmailCacheUpdater.ts | 33 ++- src/hooks/useEmailComposer.ts | 4 +- src/hooks/useEmailListActions.ts | 25 +- src/hooks/useEmailParser.ts | 6 +- src/hooks/useEmails.ts | 2 - src/hooks/useFolders.ts | 43 ++-- src/hooks/useLogout.ts | 2 +- src/hooks/useMinimizedModals.ts | 25 ++ src/hooks/useSettingsBridge.ts | 5 +- src/hooks/useThreadEmails.ts | 33 +-- src/hooks/useToast.ts | 25 ++ src/main.tsx | 50 +++- src/routes/__root.tsx | 5 +- src/routes/_baselayout.tsx | 9 - src/state/auth.ts | 1 - src/state/branding.ts | 2 +- src/state/composer.ts | 4 + src/state/emailAddress.ts | 3 +- src/state/emailComposer.ts | 3 +- src/state/flags.ts | 2 +- src/state/folders.ts | 33 ++- src/state/search.ts | 3 +- src/state/settings.ts | 1 - src/state/tourTips.ts | 4 + src/state/userDetails.ts | 8 +- src/utils/bimiService.ts | 2 +- src/utils/defaultSettings.ts | 7 +- src/utils/emailListUtils.ts | 6 +- src/utils/emailPrint.ts | 57 +++-- src/utils/emailThreading.ts | 56 +++-- src/utils/emailUtils.ts | 38 ++- src/utils/folderTree.ts | 18 +- src/utils/folderUtils.ts | 4 +- src/utils/replyForwardHelper.ts | 64 ++++- src/utils/resetLayoutCache.ts | 27 ++ src/utils/sanitizeHTMLContent.ts | 7 +- 141 files changed, 1953 insertions(+), 1420 deletions(-) create mode 100644 lint.md create mode 100644 src/components/common/minimizedModalsContext.ts create mode 100644 src/components/composer/attachmentUtils.ts create mode 100644 src/components/mailbox/composeButtonOptions.ts create mode 100644 src/components/mailbox/folderSchema.ts create mode 100644 src/components/ui/toastContext.ts create mode 100644 src/hooks/useMinimizedModals.ts create mode 100644 src/hooks/useToast.ts create mode 100644 src/utils/resetLayoutCache.ts diff --git a/lint.md b/lint.md new file mode 100644 index 0000000..dd2d888 --- /dev/null +++ b/lint.md @@ -0,0 +1,81 @@ +\E:\Apps\yukthi\Webmail-ui-OSS\src\components\common\MinimizedModalContext.tsx + 82:14 warning Fast refresh only works when a file only exports components. Use a new file to share constants or functions between components react-refresh/only-export-components + +E:\Apps\yukthi\Webmail-ui-OSS\src\components\common\TourTips.tsx + 28:14 warning Fast refresh only works when a file only exports components. Use a new file to share constants or functions between components react-refresh/only-export-components + +E:\Apps\yukthi\Webmail-ui-OSS\src\components\common\header\index.tsx + 209:6 warning React Hook useEffect has a missing dependency: 'userSettings.ui'. Either include it or remove the dependency array react-hooks/exhaustive-deps + +E:\Apps\yukthi\Webmail-ui-OSS\src\components\common\header\search\index.tsx + 242:6 warning React Hook useEffect has missing dependencies: 'PER_PAGE', 'filters', 'query', and 'setSearchState'. Either include them or remove the dependency array react-hooks/exhaustive-deps + 251:6 warning React Hook useEffect has a missing dependency: 'initialFilters'. Either include it or remove the dependency array react-hooks/exhaustive-deps + 343:6 warning React Hook useEffect has a missing dependency: 'initialFilters'. Either include it or remove the dependency array react-hooks/exhaustive-deps + 348:6 warning React Hook useEffect has a missing dependency: 'handleClearAll'. Either include it or remove the dependency array react-hooks/exhaustive-deps + +E:\Apps\yukthi\Webmail-ui-OSS\src\components\composer\AttachmentUploader.tsx + 48:14 warning Fast refresh only works when a file only exports components. Use a new file to share constants or functions between components react-refresh/only-export-components + 64:14 warning Fast refresh only works when a file only exports components. Use a new file to share constants or functions between components react-refresh/only-export-components + +E:\Apps\yukthi\Webmail-ui-OSS\src\components\composer\FolderSelectField.tsx + 121:6 warning React Hook useMemo has a missing dependency: 'renderDropdownItems'. Either include it or remove the dependency array react-hooks/exhaustive-deps + +E:\Apps\yukthi\Webmail-ui-OSS\src\components\composer\index.tsx + 56:14 warning Fast refresh only works when a file only exports components. Use a new file to share constants or functions between components react-refresh/only-export-components + 200:6 warning React Hook useEffect has a missing dependency: 'messageId'. Either include it or remove the dependency array react-hooks/exhaustive-deps + 282:6 warning React Hook useEffect has missing dependencies: 'setComposerData' and 'setDraftMessageId'. Either include them or remove the dependency array react-hooks/exhaustive-deps + 313:5 warning React Hook useCallback has a missing dependency: 'setDraftMessageId'. Either include it or remove the dependency array react-hooks/exhaustive-deps + 462:6 warning React Hook useCallback has missing dependencies: 'saveDraft' and 'setDraftMessageId'. Either include them or remove the dependency array react-hooks/exhaustive-deps + 674:5 warning React Hook useCallback has missing dependencies: 'saveDraft', 'undoTime', and 'userSettings?.general?.reply_to'. Either include them or remove the dependency array react-hooks/exhaustive-deps + 759:6 warning React Hook useCallback has a missing dependency: 'toast'. Either include it or remove the dependency array react-hooks/exhaustive-deps + 819:6 warning React Hook useCallback has missing dependencies: 'closeComposer' and 'saveDraft'. Either include them or remove the dependency array react-hooks/exhaustive-deps + 845:6 warning React Hook useCallback has a missing dependency: 'closeComposer'. Either include it or remove the dependency array react-hooks/exhaustive-deps + 955:6 warning React Hook useEffect has a missing dependency: 'setComposerData'. Either include it or remove the dependency array react-hooks/exhaustive-deps + +E:\Apps\yukthi\Webmail-ui-OSS\src\components\mailbox\ComposeButton.tsx + 23:14 warning Fast refresh only works when a file only exports components. Use a new file to share constants or functions between components react-refresh/only-export-components + +E:\Apps\yukthi\Webmail-ui-OSS\src\components\mailbox\CustomFolder.tsx + 49:14 warning Fast refresh only works when a file only exports components. Use a new file to share constants or functions between components react-refresh/only-export-components + +E:\Apps\yukthi\Webmail-ui-OSS\src\components\mailbox\EmailAttachments.tsx + 584:6 warning React Hook useEffect has a missing dependency: 'blobUrls'. Either include it or remove the dependency array react-hooks/exhaustive-deps + +E:\Apps\yukthi\Webmail-ui-OSS\src\components\mailbox\EmailComposer.tsx + 544:6 warning React Hook useEffect has missing dependencies: 'email', 'isInitialized', 'resetComposerData', and 'setComposerData'. Either include them or remove the dependency array react-hooks/exhaustive-deps + +E:\Apps\yukthi\Webmail-ui-OSS\src\components\mailbox\EmailList.tsx + 358:6 warning React Hook useEffect has missing dependencies: 'currentFolderDetail', 'folder', 'folderDetails', 'queryClient', and 'setFolderDetails'. Either include them or remove the dependency array + react-hooks/exhaustive-deps + 514:6 warning React Hook useEffect has a missing dependency: 'isMobile'. Either include it or remove the dependency array + react-hooks/exhaustive-deps + 604:6 warning React Hook useMemo has missing dependencies: 'folder', 'isFolderThread', 'searchState.isActive', 'simpleEmailArray', and 'threadedView'. Either include them or remove the dependency array + react-hooks/exhaustive-deps + 611:6 warning React Hook useEffect has a missing dependency: 'setViewingEmailFlag'. Either include it or remove the dependency array + react-hooks/exhaustive-deps + 776:6 warning React Hook useEffect has a missing dependency: 'toast'. Either include it or remove the dependency array + react-hooks/exhaustive-deps + 782:6 warning React Hook useEffect has a missing dependency: 'handleBackToList'. Either include it or remove the dependency array + react-hooks/exhaustive-deps + 782:48 warning React Hook useEffect has a complex expression in the dependency array. Extract it to a separate variable so it can be statically checked + react-hooks/exhaustive-deps + 795:6 warning React Hook useEffect has a missing dependency: 'isMobile'. Either include it or remove the dependency array + react-hooks/exhaustive-deps + 815:6 warning React Hook useEffect has missing dependencies: 'folder', 'isMobile', and 'layout'. Either include them or remove the dependency array + react-hooks/exhaustive-deps + 1532:6 warning React Hook useEffect has a missing dependency: 'handleBackToList'. Either include it or remove the dependency array + react-hooks/exhaustive-deps + 1743:5 warning React Hook useCallback has missing dependencies: 'handleBackToList', 'handleEditAsNew', 'handleForward', 'handleForwardAsAttachment', 'handleReply', 'handleReplyAll', 'handleSaveAsContactAction', 'handleSendDraft', 'handleSingleEmailDelete', 'handleSingleEmailMarkAsFlagged', 'handleSingleEmailMarkAsRead', and 'layout'. Either include them or remove the dependency array react-hooks/exhaustive-deps + 1781:6 warning React Hook useEffect has a missing dependency: 'handleRefresh'. Either include it or remove the dependency array + react-hooks/exhaustive-deps + +E:\Apps\yukthi\Webmail-ui-OSS\src\components\mailbox\MoveEmail.tsx + 87:6 warning React Hook React.useMemo has a missing dependency: 'transformFolderNode'. Either include it or remove the dependency array react-hooks/exhaustive-deps + +E:\Apps\yukthi\Webmail-ui-OSS\src\components\ui\ToastComponent.tsx + 62:14 warning Fast refresh only works when a file only exports components. Use a new file to share constants or functions between components react-refresh/only-export-components + +E:\Apps\yukthi\Webmail-ui-OSS\src\routes\$slug.tsx + 75:6 warning React Hook useEffect has missing dependencies: 'navigate' and 'slug'. Either include them or remove the dependency array react-hooks/exhaustive-deps + +✖ 39 problems (0 errors, 39 warnings) \ No newline at end of file diff --git a/src/api/admin-domain.ts b/src/api/admin-domain.ts index 49f68c1..b92fcb0 100644 --- a/src/api/admin-domain.ts +++ b/src/api/admin-domain.ts @@ -16,7 +16,7 @@ */ import { webmailStore } from '../store.ts'; -import { apiKeyAtom, csrfTokenAtom } from '../state/auth'; +import { apiKeyAtom } from '../state/auth'; import { API_URL } from './config.ts'; export type Domain = { @@ -57,7 +57,21 @@ const getHeaders = () => { }; }; -export const getDomainList = async (page = 1, size = 20, query?: string): Promise => { +export interface DomainListResponse { + data: Domain[]; + total_count: number; + current_page?: number; + total_pages?: number; + has_next?: boolean; + has_previous?: boolean; + page_size?: number; +} + +export const getDomainList = async ( + page = 1, + size = 20, + query?: string +): Promise => { const url = new URL(`${API_URL}/user/admin/domains`); url.searchParams.set('page', page.toString()); url.searchParams.set('size', size.toString()); diff --git a/src/api/auth.ts b/src/api/auth.ts index fd8c760..3952957 100644 --- a/src/api/auth.ts +++ b/src/api/auth.ts @@ -29,6 +29,10 @@ export type LoginResponse = { isVersionTwoUser: boolean; }; +export class LoginError extends Error { + status?: number; +} + export const login = async (credentials: { email: string; domain: string; @@ -45,7 +49,7 @@ export const login = async (credentials: { if (!res.ok) { const error = await res.json(); - const err = new Error(error.message || 'Login failed') as any; + const err = new LoginError(error.message || 'Login failed'); err.status = res.status; throw err; } diff --git a/src/api/config.ts b/src/api/config.ts index b067f8d..bd4ba73 100644 --- a/src/api/config.ts +++ b/src/api/config.ts @@ -18,7 +18,7 @@ // TODO: Update the api url from the environment variable or configuration file import { API_CONFIG } from '../constants/config'; -import { resetLayoutCache } from '../routes/_baselayout'; +import { resetLayoutCache } from '../utils/resetLayoutCache'; import { getCompanySlugFromPath } from '../utils/routeUtils'; export const API_URL = API_CONFIG.baseURL; diff --git a/src/api/contacts.ts b/src/api/contacts.ts index 2779e7a..86a3ad4 100644 --- a/src/api/contacts.ts +++ b/src/api/contacts.ts @@ -119,6 +119,12 @@ export const deleteContact = async (contact_id: string | number) => { return await res.json(); }; +export class BulkContactError extends Error { + status?: number; + data?: unknown; + details?: unknown; +} + export const createBulkContact = async (contactData: CreateContactData[]) => { const csrfToken = webmailStore.get(csrfTokenAtom); @@ -137,10 +143,10 @@ export const createBulkContact = async (contactData: CreateContactData[]) => { if (!res.ok) { // Changed from res.status !== 201 const errorMessage = data.message || data.error || 'Unable to create contacts'; - const error = new Error(errorMessage); - (error as any).status = res.status; - (error as any).data = data; - (error as any).details = data.details; + const error = new BulkContactError(errorMessage); + error.status = res.status; + error.data = data; + error.details = data.details; console.error('Throwing error:', error); throw error; diff --git a/src/api/fetchWrapper.ts b/src/api/fetchWrapper.ts index c054313..82b5794 100644 --- a/src/api/fetchWrapper.ts +++ b/src/api/fetchWrapper.ts @@ -47,7 +47,12 @@ export const fetchWithAuth = async (url: string, options: RequestInit = {}) => { return res; }; -export const fetchListWithAuth = async (url: string, options: any) => { +export const fetchListWithAuth = async ( + url: string, + // `timeout` isn't a real fetch() option — browsers silently ignore it — but a + // caller passes it, so it's kept in the type to avoid changing behavior here. + options: RequestInit & { timeout?: number } = {} +) => { const res = await fetch(url, { credentials: 'include', ...options, diff --git a/src/api/mailbox.ts b/src/api/mailbox.ts index 7ba5324..2881cf3 100644 --- a/src/api/mailbox.ts +++ b/src/api/mailbox.ts @@ -19,6 +19,8 @@ import { csrfTokenAtom } from '../state/auth'; import { webmailStore } from '../store.ts'; import { API_URL } from './config.ts'; import { fetchListWithAuth, fetchWithAuth } from './fetchWrapper.ts'; +import type { FolderQuota } from '../state/folders.ts'; +import type { EmailLike } from '../utils/emailThreading.ts'; export type CustomFolders = string[]; @@ -176,7 +178,7 @@ export const foldersFullPath = async (): Promise => { return body; }; -export const foldersQuota = async (path: string): Promise => { +export const foldersQuota = async (): Promise<{ quota: FolderQuota }> => { const csrfToken = webmailStore.get(csrfTokenAtom); const res = await fetchWithAuth(`${API_URL}/folder/quota?folder_path=INBOX`, { method: 'GET', @@ -202,7 +204,7 @@ export const emails = async ( full_headers: boolean = true ): Promise => { const csrfToken = webmailStore.get(csrfTokenAtom); - let sanitizedFolder = sanitizeFolderPath(folder); + const sanitizedFolder = sanitizeFolderPath(folder); const params = new URLSearchParams(); params.append('folder_path', sanitizedFolder); params.append('full_headers', full_headers.toString()); @@ -229,7 +231,7 @@ export const emailRaw = async ( ): Promise => { const csrfToken = webmailStore.get(csrfTokenAtom); const params = new URLSearchParams(); - let sanitizedFolder = sanitizeFolderPath(folderPath); + const sanitizedFolder = sanitizeFolderPath(folderPath); params.append('folder_path', sanitizedFolder); params.append('mark_as_read', String(markAsRead)); @@ -257,7 +259,7 @@ export const emailFetchByIds = async ({ }: { folderPath: string; messageIds: string[]; -}): Promise => { +}): Promise<{ emails: EmailLike[] }> => { if (!folderPath || messageIds.length === 0) { throw new Error('folderPath and messageIds are required'); } @@ -295,8 +297,8 @@ export const moveEmail = async ( body: number[] ): Promise => { const csrfToken = webmailStore.get(csrfTokenAtom); - let sanitizedSource = sanitizeFolderPath(sourceFolder); - let sanitizedDest = sanitizeFolderPath(destFolder); + const sanitizedSource = sanitizeFolderPath(sourceFolder); + const sanitizedDest = sanitizeFolderPath(destFolder); const res = await fetchWithAuth( `${API_URL}/email/move?folder_path=${path}&source_folder=${sanitizedSource}&dest_folder=${sanitizedDest}`, { @@ -368,7 +370,7 @@ export const deleteEmail = async (path: string, body: number[]): Promise => { const csrfToken = webmailStore.get(csrfTokenAtom); - let sanitizedFolder = sanitizeFolderPath(path); + const sanitizedFolder = sanitizeFolderPath(path); const res = await fetchWithAuth(`${API_URL}/email/mark/read?folder_path=${sanitizedFolder}`, { method: 'PUT', credentials: 'include', @@ -389,7 +391,7 @@ export const markReadEmail = async (path: string, body: number[]): Promise => { const csrfToken = webmailStore.get(csrfTokenAtom); - let sanitizedFolder = sanitizeFolderPath(path); + const sanitizedFolder = sanitizeFolderPath(path); const res = await fetchWithAuth(`${API_URL}/email/mark/unseen?folder_path=${sanitizedFolder}`, { method: 'PATCH', credentials: 'include', @@ -410,7 +412,7 @@ export const unmarkReadEmail = async (path: string, body: number[]): Promise => { const csrfToken = webmailStore.get(csrfTokenAtom); - let sanitizedFolder = sanitizeFolderPath(path); + const sanitizedFolder = sanitizeFolderPath(path); const res = await fetchWithAuth(`${API_URL}/email/mark/flagged?folder_path=${sanitizedFolder}`, { method: 'PUT', credentials: 'include', @@ -431,7 +433,7 @@ export const markFlaggedEmail = async (path: string, body: number[]): Promise => { const csrfToken = webmailStore.get(csrfTokenAtom); - let sanitizedFolder = sanitizeFolderPath(path); + const sanitizedFolder = sanitizeFolderPath(path); const res = await fetchWithAuth( `${API_URL}/email/mark/unflagged?folder_path=${sanitizedFolder}`, { @@ -477,7 +479,7 @@ export const createEmailFolder = async (path: string): Promise => export const deleteEmailFolder = async (path: string): Promise => { const csrfToken = webmailStore.get(csrfTokenAtom); - let sanitizedFolder = sanitizeFolderPath(path); + const sanitizedFolder = sanitizeFolderPath(path); const res = await fetchWithAuth(`${API_URL}/folder/path?folder_path=${sanitizedFolder}`, { method: 'DELETE', credentials: 'include', @@ -497,8 +499,8 @@ export const deleteEmailFolder = async (path: string): Promise => export const editEmailFolder = async (oldpath: string, newpath: string): Promise => { const csrfToken = webmailStore.get(csrfTokenAtom); - let sanitizedOldFolder = sanitizeFolderPath(oldpath); - let sanitizedNewFolder = sanitizeFolderPath(newpath); + const sanitizedOldFolder = sanitizeFolderPath(oldpath); + const sanitizedNewFolder = sanitizeFolderPath(newpath); const res = await fetchWithAuth( `${API_URL}/folder/path?old_folder_path=${sanitizedOldFolder}&new_folder_path=${sanitizedNewFolder}`, { @@ -527,7 +529,6 @@ export const folderUidValidity = async ( message: string; }> => { const csrfToken = webmailStore.get(csrfTokenAtom); - let sanitizedFolder = sanitizeFolderPath(folderPath); const res = await fetchWithAuth(`${API_URL}/folder/uid-validity?folder_path=${folderPath}`, { method: 'GET', credentials: 'include', diff --git a/src/api/search.ts b/src/api/search.ts index d973b3f..2d13bd4 100644 --- a/src/api/search.ts +++ b/src/api/search.ts @@ -21,6 +21,7 @@ import { webmailStore } from '../store'; import { API_URL, authCheck } from './config'; import { fetchWithAuth } from './fetchWrapper'; import { sanitizeFolderPath } from './mailbox'; +import type { SimplifiedEmail } from '../utils/email'; // export interface SearchRequest { // folder?: string; @@ -75,7 +76,17 @@ export interface SearchResponse { has_more: boolean; } -export const searchEmails = async (searchData: SearchRequest): Promise => { +// The real response shape (confirmed by every current consumer's `.data.data` / +// `.data.total_count` access) — SearchResponse above doesn't match what the +// endpoint actually returns and appears to be stale/aspirational. +export interface SearchApiResponse { + data: { + data: SimplifiedEmail[]; + total_count: number; + }; +} + +export const searchEmails = async (searchData: SearchRequest): Promise => { const csrfToken = webmailStore.get(csrfTokenAtom); const res = await fetchWithAuth(`${API_URL}/email/search/all`, { diff --git a/src/api/sieve.ts b/src/api/sieve.ts index e939e3a..0316b15 100644 --- a/src/api/sieve.ts +++ b/src/api/sieve.ts @@ -30,9 +30,11 @@ export interface ListScriptsApiResponse { scripts: SieveScriptList; } -// API uses tuple arrays, not objects -export type SieveFilterCondition = any[]; // e.g., ["Subject", ":matches", "Junk*"] -export type SieveFilterAction = any[]; // e.g., ["fileinto", "Junks"] +// API uses tuple arrays, not objects. Element types vary per condition/action +// kind (strings for field/operator/value, but e.g. size comparisons can carry +// numbers), so these stay loosely typed rather than guessing a fixed tuple shape. +export type SieveFilterCondition = unknown[]; // e.g., ["Subject", ":matches", "Junk*"] +export type SieveFilterAction = unknown[]; // e.g., ["fileinto", "Junks"] export interface SieveFilter { name: string; @@ -90,7 +92,7 @@ export interface UpdateFilterRequest { export interface SieveApiResponse { success: boolean; message?: string; - data?: any; + data?: unknown; } // List all scripts diff --git a/src/components/admin/domain/BrandingManager.tsx b/src/components/admin/domain/BrandingManager.tsx index 69710b9..846ed4b 100644 --- a/src/components/admin/domain/BrandingManager.tsx +++ b/src/components/admin/domain/BrandingManager.tsx @@ -16,10 +16,8 @@ */ import { useState, useRef } from 'react'; -import { useAtomValue } from 'jotai'; import { FiUpload, FiX, FiSearch, FiSave, FiEye } from 'react-icons/fi'; -import { apiKeyAtom } from '../../../state/auth'; -import { useToast } from '../../ui/ToastComponent'; +import { useToast } from '../../../hooks/useToast'; import { API_CONFIG } from '../../../constants/config'; import { BgImageService } from '../../../utils/bimiService'; @@ -109,7 +107,6 @@ function ImageUpload({ } export default function BrandingManager() { - const apiKey = useAtomValue(apiKeyAtom); const toast = useToast(); const [slug, setSlug] = useState(''); diff --git a/src/components/admin/domain/BulkEdit.tsx b/src/components/admin/domain/BulkEdit.tsx index d80efb1..700f242 100644 --- a/src/components/admin/domain/BulkEdit.tsx +++ b/src/components/admin/domain/BulkEdit.tsx @@ -25,7 +25,6 @@ import { FiAlertCircle, FiCheckCircle, FiDownload, - FiLoader, FiInfo, FiChevronDown, FiChevronUp, @@ -166,8 +165,9 @@ function parseFile(file: File): Promise<{ rows: ParsedRow[]; missingHeaders: str const rows = normalised.map((row, i) => validateRow(row, i + 2)); resolve({ rows, missingHeaders: [] }); - } catch (err: any) { - reject(new Error(`Failed to parse file: ${err.message}`)); + } catch (err) { + const message = err instanceof Error ? err.message : String(err); + reject(new Error(`Failed to parse file: ${message}`)); } }; @@ -280,8 +280,8 @@ export default function DomainBulkEditModal({ isOpen, onClose }: Props) { } setRows(parsed); setPhase('parsed'); - } catch (err: any) { - setParseError(err.message); + } catch (err) { + setParseError(err instanceof Error ? err.message : String(err)); } }, []); @@ -333,7 +333,7 @@ export default function DomainBulkEditModal({ isOpen, onClose }: Props) { successes++; resolve(); }, - onError: (err: any) => { + onError: (err) => { errors.push({ domain: row.domain, rowIndex: row._rowIndex, diff --git a/src/components/admin/domain/BulkImport.tsx b/src/components/admin/domain/BulkImport.tsx index 4962dda..cb19a55 100644 --- a/src/components/admin/domain/BulkImport.tsx +++ b/src/components/admin/domain/BulkImport.tsx @@ -25,7 +25,6 @@ import { FiAlertCircle, FiCheckCircle, FiDownload, - FiLoader, FiInfo, FiChevronDown, FiChevronUp, @@ -166,8 +165,9 @@ function parseFile(file: File): Promise<{ rows: ParsedRow[]; missingHeaders: str const rows = normalised.map((row, i) => validateRow(row, i + 2)); // +2 for header + 1-based resolve({ rows, missingHeaders: [] }); - } catch (err: any) { - reject(new Error(`Failed to parse file: ${err.message}`)); + } catch (err) { + const message = err instanceof Error ? err.message : String(err); + reject(new Error(`Failed to parse file: ${message}`)); } }; @@ -227,23 +227,6 @@ function ProgressRing({ pct }: { pct: number }) { ); } -function ValidationBadge({ count, type }: { count: number; type: 'error' | 'ok' }) { - if (type === 'error') { - return ( - - - {count} invalid - - ); - } - return ( - - - {count} valid - - ); -} - // ─── Main Modal ─────────────────────────────────────────────────────────────── interface Props { @@ -294,8 +277,8 @@ export default function DomainBulkImportModal({ isOpen, onClose }: Props) { } setRows(parsed); setPhase('parsed'); - } catch (err: any) { - setParseError(err.message); + } catch (err) { + setParseError(err instanceof Error ? err.message : String(err)); } }, []); @@ -327,7 +310,7 @@ export default function DomainBulkImportModal({ isOpen, onClose }: Props) { setCurrentDomain(row.domain); try { - await new Promise((resolve, reject) => { + await new Promise((resolve) => { createMutation.mutate( { domain: row.domain, @@ -339,13 +322,13 @@ export default function DomainBulkImportModal({ isOpen, onClose }: Props) { sieve_port: row.sieve_port, is_active: row.is_active, is_v2_user: row.is_v2_user, - } as any, + }, { onSuccess: () => { successes++; resolve(); }, - onError: (err: any) => { + onError: (err) => { errors.push({ domain: row.domain, rowIndex: row._rowIndex, diff --git a/src/components/admin/domain/DomainDetailsModal.tsx b/src/components/admin/domain/DomainDetailsModal.tsx index f08ba75..3653ece 100644 --- a/src/components/admin/domain/DomainDetailsModal.tsx +++ b/src/components/admin/domain/DomainDetailsModal.tsx @@ -22,7 +22,7 @@ import type { Domain } from '../../../api/admin-domain'; interface DomainDetailsModalProps { isOpen: boolean; onClose: () => void; - data: any; + data: Domain | null; } const DomainDetailsModal = ({ isOpen, onClose, data }: DomainDetailsModalProps) => { diff --git a/src/components/admin/domain/DomainForm.tsx b/src/components/admin/domain/DomainForm.tsx index 8686427..cf81922 100644 --- a/src/components/admin/domain/DomainForm.tsx +++ b/src/components/admin/domain/DomainForm.tsx @@ -24,9 +24,9 @@ import { FiArrowLeft } from 'react-icons/fi'; import { Flex, Switch, Text } from '@radix-ui/themes'; import { Controller } from 'react-hook-form'; import { useCreateDomain, useUpdateDomain, useGetDomain } from '../../../hooks/useAdminDomain'; -import { useToast } from '../../ui/ToastComponent'; import { apiKeyAtom } from '../../../state/auth'; import { useAtomValue } from 'jotai'; +import type { CreateDomainData } from '../../../api/admin-domain'; const schema = yup.object().shape({ domain: yup.string().required('Domain name is required'), @@ -66,7 +66,6 @@ function DomainForm() { const { data: domainData, isLoading } = useGetDomain(params.domain); const createMutation = useCreateDomain(); const updateMutation = useUpdateDomain(); - const toast = useToast(); const { register, @@ -116,7 +115,7 @@ function DomainForm() { } ); } else { - createMutation.mutate(data as any, { + createMutation.mutate(data as unknown as CreateDomainData, { onSuccess: () => { navigate({ to: '/1219/admin/domain' }); }, @@ -382,7 +381,15 @@ function DomainForm() { ); } -const FormField = ({ label, error, children }: any) => ( +const FormField = ({ + label, + error, + children, +}: { + label: string; + error?: string; + children: React.ReactNode; +}) => (
- ); -}); + ); + } +); SlashCommandList.displayName = 'SlashCommandList'; diff --git a/src/components/composer/contentEditor/SlashCommands.ts b/src/components/composer/contentEditor/SlashCommands.ts index 3aeb5e2..595e786 100644 --- a/src/components/composer/contentEditor/SlashCommands.ts +++ b/src/components/composer/contentEditor/SlashCommands.ts @@ -15,22 +15,14 @@ * . */ -import { Extension } from '@tiptap/core'; -import Suggestion from '@tiptap/suggestion'; +import { Extension, type Editor, type Range } from '@tiptap/core'; +import Suggestion, { type SuggestionProps, type SuggestionKeyDownProps } from '@tiptap/suggestion'; import { PluginKey } from '@tiptap/pm/state'; import { ReactRenderer } from '@tiptap/react'; import tippy, { type Instance } from 'tippy.js'; import React from 'react'; -import { SlashCommandList, type CommandItem } from './SlashCommandList'; -import { - FaHeading, - FaListUl, - FaListOl, - FaTable, - FaImage, - FaQuoteRight, - FaCode, -} from 'react-icons/fa6'; +import { SlashCommandList, type CommandItem, type SlashCommandListRef } from './SlashCommandList'; +import { FaListUl, FaListOl, FaTable } from 'react-icons/fa6'; const getSuggestionItems = ({ query }: { query: string }): CommandItem[] => { const items: CommandItem[] = [ @@ -94,7 +86,7 @@ export const SlashCommands = Extension.create({ return { suggestion: { char: '/', - command: ({ editor, range, props }: any) => { + command: ({ editor, range, props }: { editor: Editor; range: Range; props: CommandItem }) => { // Execute the item's command props.command({ editor, range }); }, @@ -111,11 +103,11 @@ export const SlashCommands = Extension.create({ command: this.options.suggestion.command, items: getSuggestionItems, render: () => { - let component: ReactRenderer; + let component: ReactRenderer; let popup: Instance[]; return { - onStart: (props: any) => { + onStart: (props: SuggestionProps) => { component = new ReactRenderer(SlashCommandList, { props, editor: props.editor, @@ -126,7 +118,7 @@ export const SlashCommands = Extension.create({ } popup = tippy('body', { - getReferenceClientRect: props.clientRect, + getReferenceClientRect: props.clientRect as () => DOMRect, appendTo: () => document.body, content: component.element, showOnCreate: true, @@ -136,7 +128,7 @@ export const SlashCommands = Extension.create({ }); }, - onUpdate(props: any) { + onUpdate(props: SuggestionProps) { component.updateProps(props); if (!props.clientRect) { @@ -144,17 +136,17 @@ export const SlashCommands = Extension.create({ } popup[0].setProps({ - getReferenceClientRect: props.clientRect, + getReferenceClientRect: props.clientRect as () => DOMRect, }); }, - onKeyDown(props: any) { + onKeyDown(props: SuggestionKeyDownProps) { if (props.event.key === 'Escape') { popup[0].hide(); return true; } - return component.ref?.onKeyDown(props); + return component.ref?.onKeyDown({ event: props.event }) ?? false; }, onExit() { diff --git a/src/components/composer/contentEditor/index.tsx b/src/components/composer/contentEditor/index.tsx index 52cad93..c8963ae 100644 --- a/src/components/composer/contentEditor/index.tsx +++ b/src/components/composer/contentEditor/index.tsx @@ -27,11 +27,11 @@ import { TableCell } from '@tiptap/extension-table-cell'; import TableContextMenu from './TableContextMenu'; import Image from '@tiptap/extension-image'; import Link from '@tiptap/extension-link'; -import { Node, mergeAttributes, Mark } from '@tiptap/core'; +import { Node, mergeAttributes, Mark, type CommandProps } from '@tiptap/core'; import { TextStyle } from '@tiptap/extension-text-style'; import Color from '@tiptap/extension-color'; import Blockquote from '@tiptap/extension-blockquote'; -import { useEffect, useRef, useState } from 'react'; +import { useEffect, useState } from 'react'; import { useAtomValue } from 'jotai'; import { userSettingsAtom } from '../../../state/settings'; import { OrderedList } from '@tiptap/extension-ordered-list'; @@ -93,32 +93,32 @@ const CustomTextStyle = TextStyle.extend({ ...this.parent?.(), setFontSize: (fontSize: string) => - ({ chain }: any) => + ({ chain }: CommandProps) => chain().setMark('textStyle', { fontSize }).run(), unsetFontSize: () => - ({ chain }: any) => + ({ chain }: CommandProps) => chain().setMark('textStyle', { fontSize: null }).run(), setBackgroundColor: (backgroundColor: string) => - ({ chain }: any) => + ({ chain }: CommandProps) => chain().setMark('textStyle', { backgroundColor }).run(), unsetBackgroundColor: () => - ({ chain }: any) => + ({ chain }: CommandProps) => chain().setMark('textStyle', { backgroundColor: null }).run(), setFontFamily: (fontFamily: string) => - ({ chain }: any) => + ({ chain }: CommandProps) => chain().setMark('textStyle', { fontFamily }).run(), unsetFontFamily: () => - ({ chain }: any) => + ({ chain }: CommandProps) => chain().setMark('textStyle', { fontFamily: null }).run(), }; }, diff --git a/src/components/composer/index.tsx b/src/components/composer/index.tsx index 73ca6f1..2f4beb0 100644 --- a/src/components/composer/index.tsx +++ b/src/components/composer/index.tsx @@ -15,36 +15,31 @@ * . */ -import { Box, Button, Flex, Grid, Separator, Dialog, Text, AlertDialog } from '@radix-ui/themes'; -import { atom, useAtom, useAtomValue, useSetAtom } from 'jotai'; +import { Box, Button, Flex, Dialog, Text, AlertDialog } from '@radix-ui/themes'; +import { useAtom, useAtomValue, useSetAtom } from 'jotai'; import { composerDataAtom, composerOpenAtom, resetComposerDataAtom, + draftMessageIdAtom, type EmailAddress, + type ComposedEmailData, } from '../../state/composer'; import { FaDeleteLeft, FaFloppyDisk, FaPaperPlane } from 'react-icons/fa6'; import { useEffect, useState, useCallback, useRef, useMemo } from 'react'; -import { v4 as uuidv4 } from 'uuid'; import RecipientField, { type RecipientFieldHandle } from './RecipientField'; import SubjectField from './SubjectField'; import ContentEditor from './contentEditor'; -import AttachmentUploader, { - toBase64, - MAX_TOTAL_SIZE, - MAX_INDIVIDUAL_FILE_SIZE, - formatFileSize, -} from './AttachmentUploader'; +import AttachmentUploader from './AttachmentUploader'; +import { toBase64, MAX_TOTAL_SIZE, MAX_INDIVIDUAL_FILE_SIZE, formatFileSize } from './attachmentUtils'; import { useDropzone } from 'react-dropzone'; import { FaPaperclip } from 'react-icons/fa6'; import { formatComposedEmailData } from '../../utils/composedDataFormat'; import EmailPriorityField from './EmailPriorityField'; import { useDraftMail, useSendMail } from '../../hooks/useComposer'; -import FolderSelectField from './FolderSelectField'; -import DialogWrapper from '../common/Dialoge'; import { emailAddress } from '../../state/emailAddress'; import { userSettingsAtom } from '../../state/settings'; -import { useToast } from '../ui/ToastComponent'; +import { useToast } from '../../hooks/useToast'; import TemplateSelector from './TempelateSelector'; import { generateMessageId, sendMailV2 } from '../../api/composer'; import { userDetailsAtom } from '../../state/userDetails'; @@ -55,14 +50,12 @@ import { SEND_DEFAULT } from '../../constants/constant'; import { useIsMobile } from '../../hooks/use-mobile'; import { getEditorDimensions } from '../../utils/dimensions'; -export const draftMessageIdAtom = atom(null); - export type EmailPriority = 'normal' | 'high' | 'low'; // Types for localStorage data interface LocalStorageDraft { messageId: string; - composerData: any; + composerData: ComposedEmailData; priority: EmailPriority; folder: string; timestamp: number; @@ -72,12 +65,12 @@ interface LocalStorageDraft { const DRAFT_STORAGE_KEY = 'email-composer-draft'; // Debounce hook with proper typing -const useDebounce = (callback: (...args: any[]) => void, delay: number) => { +const useDebounce = (callback: (...args: Args) => void, delay: number) => { const timeoutRef = useRef(null); const cancelRef = useRef<() => void>(() => {}); const debouncedFunction = useCallback( - (...args: any[]) => { + (...args: Args) => { if (timeoutRef.current) { window.clearTimeout(timeoutRef.current); } @@ -122,7 +115,7 @@ const Composer = () => { const [fullViewEnabled, setFullViewEnabled] = useState(false); const [draftMessageId, setDraftMessageId] = useAtom(draftMessageIdAtom); const [messageId, setMessageId] = useState(''); - const [sentMessageId, setSentMessageId] = useState(''); + const [, setSentMessageId] = useState(''); const [undoTime, setUndoTime] = useState(5000); const [saveDraft, setSaveDraft] = useState('Drafts'); const [folder, setFolder] = useState('Sent'); @@ -147,7 +140,7 @@ const Composer = () => { // Add state for close confirmation dialog const [showCloseConfirm, setShowCloseConfirm] = useState(false); - const [pendingCloseAction, setPendingCloseAction] = useState<(() => void) | null>(null); + const [, setPendingCloseAction] = useState<(() => void) | null>(null); // Add state to track if there are unsaved changes // const [hasUnsavedChanges, setHasUnsavedChanges] = useState(false); @@ -171,7 +164,10 @@ const Composer = () => { // Track if we've already auto-saved to prevent duplicate saves const hasAutoSavedRef = useRef(false); // Track pending email to be sent - const pendingEmailRef = useRef(null); + const pendingEmailRef = useRef<{ + mailData: ReturnType; + currentMsgId: string; + } | null>(null); // Track if we're in the undo period const isUndoPeriodRef = useRef(false); // Track auto-save attempts to prevent too many API calls @@ -278,11 +274,18 @@ const Composer = () => { isSendingRef.current = false; isManualDraftSaveRef.current = false; } - }, [openComposer, userDetails?.domain, isQuotaExceeded, toast]); + }, [ + openComposer, + userDetails?.domain, + isQuotaExceeded, + toast, + setComposerData, + setDraftMessageId, + ]); // Save to localStorage - UPDATED to use current messageId const saveToLocalStorage = useCallback( - (data: any, currentPriority: EmailPriority, currentFolder: string) => { + (data: ComposedEmailData, currentPriority: EmailPriority, currentFolder: string) => { // Don't save if quota is exceeded if (isQuotaExceeded) { return; @@ -309,7 +312,7 @@ const Composer = () => { console.error('Error saving to localStorage:', error); } }, - [userDetails?.domain, isQuotaExceeded] + [userDetails?.domain, isQuotaExceeded, setDraftMessageId] ); // Remove from localStorage @@ -452,7 +455,7 @@ const Composer = () => { onSuccess: () => { autoSaveCountRef.current++; }, - onError: (err: any) => { + onError: (err) => { console.error('Failed to auto-save draft:', err); }, }); @@ -475,9 +478,7 @@ const Composer = () => { ({ isDraft, folderPath, - successTitle, successDescription, - errorTitle, allowUndo = false, dataOverrides = {}, }: { @@ -487,7 +488,7 @@ const Composer = () => { successDescription: string; errorTitle: string; allowUndo?: boolean; - dataOverrides?: any; + dataOverrides?: Partial; }) => { // Check if trying to save draft when quota is exceeded if (isDraft && isQuotaExceeded) { @@ -610,13 +611,14 @@ const Composer = () => { setDraftMessageId(null); setSentMessageId(''); isSendingRef.current = false; - } catch (err: any) { + } catch (err) { isSendInFlightRef.current = false; toast.dismiss(loadingId); window.removeEventListener('beforeunload', handleBeforeUnload); toast.error({ description: - err?.message || 'An error occurred while sending mail. Please try again.', + (err instanceof Error && err.message) || + 'An error occurred while sending mail. Please try again.', }); setSentMessageId(''); setOpenComposer(true); @@ -651,11 +653,10 @@ const Composer = () => { hasAutoSavedRef.current = false; autoSaveCountRef.current = 0; }, - onError: (err: any) => { + onError: (err) => { if (loadingId) toast.dismiss(loadingId); toast.error({ - description: - err?.message || 'An error occurred while sending mail. Please try again.', + description: err?.message || 'An error occurred while sending mail. Please try again.', }); // On error, clear the sent message ID if (!isDraft) { @@ -693,7 +694,7 @@ const Composer = () => { // Function to actually send the email (after confirmation) const sendEmailConfirmed = useCallback( - (overrides: any = {}) => { + (overrides: Partial = {}) => { // Always generate a NEW Message-ID for sending, regardless of draft status const sendMessageId = generateMessageId(userDetails?.domain || ''); setSentMessageId(sendMessageId); @@ -742,7 +743,7 @@ const Composer = () => { } // 4. Prepare data overrides - const overrides: any = {}; + const overrides: Partial = {}; if (pendingTo) overrides.to = effectiveTo; if (pendingCc) overrides.cc = [...(composerData.cc || []), pendingCc]; if (pendingBcc) overrides.bcc = [...(composerData.bcc || []), pendingBcc]; @@ -757,7 +758,14 @@ const Composer = () => { // Send immediately if message has content sendEmailConfirmed(overrides); } - }, [isMessageEmpty, sendEmailConfirmed, composerData.to, composerData.cc, composerData.bcc]); + }, [ + isMessageEmpty, + sendEmailConfirmed, + composerData.to, + composerData.cc, + composerData.bcc, + toast, + ]); const handleDraftButtonClick = useCallback(() => { // Don't save draft if quota is exceeded @@ -800,7 +808,7 @@ const Composer = () => { removeFromLocalStorage(); // 4. Prepare data overrides - const overrides: any = {}; + const overrides: Partial = {}; if (pendingTo) overrides.to = effectiveTo; if (pendingCc) overrides.cc = [...(composerData.cc || []), pendingCc]; if (pendingBcc) overrides.bcc = [...(composerData.bcc || []), pendingBcc]; @@ -933,15 +941,19 @@ const Composer = () => { // Initialize composer data with signature useEffect(() => { if (openComposer && signatureHtml && !composerData.html) { - setComposerData((prev: any) => { - const from = prev.from || {}; - const sanitizeString = (value: any): string => { + setComposerData((prev) => { + const from = prev.from || ({} as Partial); + const sanitizeString = (value: unknown): string => { return typeof value === 'string' ? value.trim() : ''; }; return { ...prev, from: { - address: sanitizeString(from?.address) || currentEmail || '', + // currentEmail is an EmailAddress object, not a string — if + // sanitizeString(from?.address) is empty this assigns the whole + // object here (objects are always truthy), not a string. Pre-existing, + // preserved as-is; see CLAUDE.md. + address: (sanitizeString(from?.address) || currentEmail || '') as string, name: sanitizeString(from?.name) || 'Unknown Sender', }, html: prev.html || signatureHtml, @@ -949,7 +961,7 @@ const Composer = () => { }; }); } - }, [openComposer, signatureHtml, currentEmail, composerData.html]); + }, [openComposer, signatureHtml, currentEmail, composerData.html, setComposerData]); const handleTemplateSelect = useCallback( (templateData: { subject: string; html: string }) => { @@ -965,7 +977,7 @@ const Composer = () => { [signatureHtml, updateComposerData] ); - const handleToemailChange = (emailAddresses: any) => { + const handleToemailChange = (emailAddresses: EmailAddress[]) => { updateComposerData({ to: emailAddresses }); }; @@ -982,7 +994,7 @@ const Composer = () => { const onDrop = useCallback( async (acceptedFiles: File[]) => { const currentTotalSize = (composerData.attachments || []).reduce( - (sum: number, file: any) => sum + (file.size || 0), + (sum, file) => sum + (file.size || 0), 0 ); const newFilesTotalSize = acceptedFiles.reduce((sum, file) => sum + file.size, 0); @@ -1017,7 +1029,7 @@ const Composer = () => { })) ); - setComposerData((prev: any) => ({ + setComposerData((prev) => ({ ...prev, attachments: [...(prev.attachments || []), ...base64Attachments], })); @@ -1026,7 +1038,7 @@ const Composer = () => { description: `Added ${validFiles.length} file(s)`, duration: 3000, }); - } catch (error) { + } catch { toast.error({ description: 'Failed to process files.' }); } }, @@ -1073,7 +1085,10 @@ const Composer = () => { onClick={handleDraftButtonClick} className={`text-[var(--gray-11)] hover:text-[var(--gray-12)] hover:bg-[var(--gray-3)] ${ isQuotaExceeded || - (!composerData.to?.length && !(toRef.current as any)?.inputValue) + // RecipientFieldHandle doesn't actually expose `inputValue` (only `flush`), + // so this always evaluates truthy — pre-existing no-op, not fixed here. + (!composerData.to?.length && + !(toRef.current as unknown as { inputValue?: string })?.inputValue) ? 'opacity-50 cursor-not-allowed' : '' }`} diff --git a/src/components/contacts/BulkCreateView.tsx b/src/components/contacts/BulkCreateView.tsx index 1210cde..37de3c8 100644 --- a/src/components/contacts/BulkCreateView.tsx +++ b/src/components/contacts/BulkCreateView.tsx @@ -15,12 +15,12 @@ * . */ -import { Flex, TextField, Button, Box, Table, Dialog } from '@radix-ui/themes'; -import { FaPlus, FaTrash, FaTimes, FaInfoCircle } from 'react-icons/fa'; +import { Flex, TextField, Button, Table, Dialog } from '@radix-ui/themes'; +import { FaPlus, FaTrash, FaTimes } from 'react-icons/fa'; import DropdownWrapper from '../common/DropdownWrapper'; import type { CreateContactData } from '../../utils/contact'; import { useState, useEffect } from 'react'; -import { FaEllipsis, FaEllipsisVertical } from 'react-icons/fa6'; +import { FaEllipsisVertical } from 'react-icons/fa6'; interface BulkCreateViewProps { open: boolean; @@ -92,7 +92,7 @@ export const BulkCreateView = ({ // Phone validation if ( contact.phone && - !/^[\+]?[1-9][\d]{0,15}$/.test(contact.phone.replace(/[\s\-\(\)]/g, '')) + !/^[+]?[1-9][\d]{0,15}$/.test(contact.phone.replace(/[\s\-()]/g, '')) ) { contactErrors.phone = 'Invalid phone format'; } diff --git a/src/components/contacts/DeleteConfirm.tsx b/src/components/contacts/DeleteConfirm.tsx index 730a126..4f5a853 100644 --- a/src/components/contacts/DeleteConfirm.tsx +++ b/src/components/contacts/DeleteConfirm.tsx @@ -15,7 +15,7 @@ * . */ -import { Dialog, Button, Flex, Box } from '@radix-ui/themes'; +import { Dialog, Button, Flex } from '@radix-ui/themes'; import { FaTrash, FaExclamationTriangle, FaTimes } from 'react-icons/fa'; import type { Contact } from '../../utils/contact'; diff --git a/src/components/contacts/index.tsx b/src/components/contacts/index.tsx index 7ff5724..2ba1c34 100644 --- a/src/components/contacts/index.tsx +++ b/src/components/contacts/index.tsx @@ -38,7 +38,7 @@ import DropdownWrapper from '../common/DropdownWrapper'; import type { Contact, ContactsResponse, CreateContactData } from '../../utils/contact'; import CSVImportModal from './CSVImport'; import { deleteContact, getContacts } from '../../api/contacts'; -import { useToast } from '../ui/ToastComponent'; +import { useToast } from '../../hooks/useToast'; import { exportToCSV, exportToVCard, @@ -74,15 +74,11 @@ function ContactPage() { { name: '', email: '', phone: '', notes: '' }, ]); - const { data, isPending, refetch, error, isError } = useContacts(page, perPage); + const { data, isPending, refetch, isError } = useContacts(page, perPage); const { mutate: createSingleContact, isPending: isLoadingCreateSingleContact } = useCreateContact(); const { mutate: editSingleContact, isPending: isLoadingEditSingleContact } = useEditContact(); - const { - mutate: createBulkContact, - isPending: isLoadingBulkCreate, - error: bulkCreateError, - } = useCreateBulkContact(); + const { mutate: createBulkContact, isPending: isLoadingBulkCreate } = useCreateBulkContact(); const { mutate: deleteSingleContact, isPending: isLoadingDelete } = useDeleteContact(); const contactsData = data as ContactsResponse; @@ -122,7 +118,11 @@ function ContactPage() { async (pageNum: number) => { const response = await getContacts(pageNum, 100); return { - data: response.data as any, + // api/contacts.ts's Contact (nullable phone/notes/contact_id) differs + // slightly from utils/contact.ts's Contact (non-nullable) — pre-existing + // mismatch between the two Contact shapes, bridged via cast rather than + // reconciled here. + data: response.data as unknown as Contact[], total_pages: response.total_pages, }; }, @@ -275,10 +275,14 @@ function ContactPage() { description: `Successfully created ${validContacts.length} contact${validContacts.length > 1 ? 's' : ''}`, }); }, - onError: (error: any) => { + onError: (error) => { + // error is a plain Error here, so .response is never actually present — + // pre-existing defensive fallback for a shape this API doesn't produce. + const response = (error as unknown as { response?: { data?: { message?: string; error?: string } } }) + .response; const errorMessage = - error?.response?.data?.message || - error?.response?.data?.error || + response?.data?.message || + response?.data?.error || error?.message || 'Failed to create contacts. Please try again.'; @@ -306,12 +310,14 @@ function ContactPage() { description: `Successfully imported contact${contacts.length > 1 ? 's' : ''}.`, }); }, - onError: (error: any) => { + onError: (error) => { console.error('CSV import failed:', error); toast.dismiss(loadingId); + const response = (error as unknown as { response?: { data?: { message?: string; error?: string } } }) + .response; const errorMessage = - error?.response?.data?.message || - error?.response?.data?.error || + response?.data?.message || + response?.data?.error || error?.message || 'Failed to import contacts from CSV. Please try again.'; diff --git a/src/components/help/index.tsx b/src/components/help/index.tsx index 22d21af..deee9d5 100644 --- a/src/components/help/index.tsx +++ b/src/components/help/index.tsx @@ -16,11 +16,11 @@ */ import React from 'react'; -import { Heading, Text, Badge } from '@radix-ui/themes'; +import { Heading, Badge } from '@radix-ui/themes'; +import type { IconType } from 'react-icons'; import { HiOutlineUsers, HiOutlineVideoCamera, - HiOutlineCog6Tooth, HiOutlineFolder, HiOutlinePencilSquare, HiOutlineLightBulb, @@ -105,7 +105,7 @@ const FeatureCard = ({ title, description, }: { - icon: any; + icon: IconType; title: string; description: string; }) => ( diff --git a/src/components/login/Login.tsx b/src/components/login/Login.tsx index c91b0e1..41cd23d 100644 --- a/src/components/login/Login.tsx +++ b/src/components/login/Login.tsx @@ -22,7 +22,7 @@ import { yupResolver } from '@hookform/resolvers/yup'; import * as yup from 'yup'; import background from '../../assets/sign-in.svg'; import { useNavigate } from '@tanstack/react-router'; -import { useToast } from '../ui/ToastComponent'; +import { useToast } from '../../hooks/useToast'; import { useState, useEffect, useRef } from 'react'; import { userDetailsAtom } from '../../state/userDetails'; import { useAtom } from 'jotai'; @@ -207,12 +207,12 @@ const Login = ({ branding }: { branding?: LoginBranding }) => { recaptcha_token: recaptchaToken, }, { - onSuccess: (_response: any) => { + onSuccess: () => { setUserDetails({ email: data.email, domain }); recaptchaRef.current?.reset(); setRecaptchaToken(null); }, - onError: (error: any) => { + onError: (error) => { setIsLoading(false); // Reset reCAPTCHA on error diff --git a/src/components/login/LoginNoRecaptcha.tsx b/src/components/login/LoginNoRecaptcha.tsx index ee74f49..983b975 100644 --- a/src/components/login/LoginNoRecaptcha.tsx +++ b/src/components/login/LoginNoRecaptcha.tsx @@ -22,7 +22,7 @@ import { yupResolver } from '@hookform/resolvers/yup'; import * as yup from 'yup'; import background from '../../assets/sign-in.svg'; import { useNavigate, Link } from '@tanstack/react-router'; // Added Link import -import { useToast } from '../ui/ToastComponent'; +import { useToast } from '../../hooks/useToast'; import { useState } from 'react'; import { userDetailsAtom } from '../../state/userDetails'; import { useAtom } from 'jotai'; @@ -30,7 +30,6 @@ import { MdEmail, MdMailOutline } from 'react-icons/md'; import { FaEye, FaEyeSlash, - FaGlobe, FaShieldAlt, FaFileAlt, FaTable, @@ -141,7 +140,7 @@ const Login = () => { navigate({ to: '/' }); setUserDetails({ email: data.email, domain }); }, - onError: (error: any) => { + onError: (error) => { setIsLoading(false); toast.error({ description: error.message || 'Failed to submit form. Please try again.', diff --git a/src/components/mailbox/ComposeButton.tsx b/src/components/mailbox/ComposeButton.tsx index c153ff3..2d4b619 100644 --- a/src/components/mailbox/ComposeButton.tsx +++ b/src/components/mailbox/ComposeButton.tsx @@ -20,16 +20,6 @@ import { useEffect } from 'react'; export type ComposeButtonStyle = 'default' | 'compact' | 'minimal'; -export const COMPOSE_BUTTON_OPTIONS: { - value: ComposeButtonStyle; - label: string; - description: string; -}[] = [ - { value: 'default', label: 'Default', description: 'Solid with shadow' }, - { value: 'compact', label: 'Compact', description: 'Solid, no shadow' }, - { value: 'minimal', label: 'Minimal', description: 'Ghost style' }, -]; - const variantConfig = { default: { expanded: diff --git a/src/components/mailbox/CreateFolder.tsx b/src/components/mailbox/CreateFolder.tsx index fe49cc4..56145ac 100644 --- a/src/components/mailbox/CreateFolder.tsx +++ b/src/components/mailbox/CreateFolder.tsx @@ -24,7 +24,7 @@ import * as yup from 'yup'; import { yupResolver } from '@hookform/resolvers/yup'; import { FormProvider, useForm } from 'react-hook-form'; import { useCreateEmailFolder } from '../../hooks/useEmails'; -import { useToast } from '../ui/ToastComponent'; +import { useToast } from '../../hooks/useToast'; import { useQueryClient } from '@tanstack/react-query'; import { useEffect } from 'react'; // Added useEffect @@ -64,7 +64,7 @@ function CreateFolder() { setOpen(false); methods.reset(); }, - onError: (error: any) => { + onError: (error) => { toast.dismiss(loadingId); toast.error({ description: error.message || 'Failed to create folder. Please try again.', diff --git a/src/components/mailbox/CustomFolder.tsx b/src/components/mailbox/CustomFolder.tsx index 029a2aa..025a0fd 100644 --- a/src/components/mailbox/CustomFolder.tsx +++ b/src/components/mailbox/CustomFolder.tsx @@ -28,6 +28,7 @@ import { MAX_FOLDER_DEPTH } from '../../constants/constant'; import * as yup from 'yup'; import { yupResolver } from '@hookform/resolvers/yup'; import { sortFoldersAscending } from '../../utils/folderUtils'; +import { folderSchema } from './folderSchema'; const cleanFolderName = (folderName: string): string => { return folderName.replace(/^"(.+)"$/, '$1').replace(/\\"/g, '"'); @@ -46,18 +47,6 @@ interface CustomFolderProps { isDragging?: boolean; } -export const folderSchema = yup.object({ - newFolderName: yup - .string() - .required('Folder name is required') - .min(1, 'Name is too short') - .max(30, 'Name is too long') - .matches( - /^[a-zA-Z0-9](?:[a-zA-Z0-9\s-_]*[a-zA-Z0-9])?$/, - 'Only letters, numbers, spaces, hyphens, and underscores are allowed. Cannot start or end with a space/symbol.' - ), -}); - type FolderFormData = yup.InferType; const CustomFolder = ({ diff --git a/src/components/mailbox/DefaultFolder.tsx b/src/components/mailbox/DefaultFolder.tsx index f1d51b8..ac6d6e9 100644 --- a/src/components/mailbox/DefaultFolder.tsx +++ b/src/components/mailbox/DefaultFolder.tsx @@ -18,8 +18,10 @@ import type { EmailFolder } from '../../api/mailbox'; import type { IconType } from 'react-icons/lib'; import type { FolderNode } from '../../utils/folderTree'; +import type { UserSettings } from '../../api/user'; import FolderItem from './FolderItem'; -import CustomFolder, { folderSchema } from './CustomFolder'; +import CustomFolder from './CustomFolder'; +import { folderSchema } from './folderSchema'; import { useState } from 'react'; import { useForm } from 'react-hook-form'; import { Button, Flex, TextField } from '@radix-ui/themes'; @@ -39,7 +41,7 @@ interface FolderProps { isCollapsed?: boolean; onDrop?: (folderPath: string) => void; isDragging?: boolean; - folderPops?: any; + folderPops?: Partial; folderNode?: FolderNode; onAddFolder?: (parentFolder: string, newFolderName: string) => void; onEdit?: (oldName: string, newName: string) => void; diff --git a/src/components/mailbox/EmailActions.tsx b/src/components/mailbox/EmailActions.tsx index 67b8bb0..372c474 100644 --- a/src/components/mailbox/EmailActions.tsx +++ b/src/components/mailbox/EmailActions.tsx @@ -39,14 +39,15 @@ import { } from 'react-icons/md'; import DropdownWrapper from '../common/DropdownWrapper'; import { FaEllipsisVertical, FaRotateRight } from 'react-icons/fa6'; +import type { EmailLike } from '../../utils/emailThreading'; interface EmailActionsProps { - email: any; - onReply: (email: any) => void; - onEditAsNew: (email: any) => void; - onSaveAsTemplate: (email: any) => void; - onReplyAll: (email: any) => void; - onForward: (email: any) => void; + email: EmailLike; + onReply: (email: EmailLike) => void; + onEditAsNew: (email: EmailLike) => void; + onSaveAsTemplate: (email: EmailLike) => void; + onReplyAll: (email: EmailLike) => void; + onForward: (email: EmailLike) => void; onForwardAsAttachment?: () => void; onDelete: (emailId: string) => void; onMove?: () => void; @@ -56,10 +57,10 @@ interface EmailActionsProps { handleSingleEmailMarkAsRead?: (emailId: string, action: boolean) => void; reFetchMails?: () => void; - onPrint?: (email: any) => void; - onDownload?: (email: any) => void; - onViewInWindow?: (email: any) => void; - onViewInRaw?: (email: any) => void; + onPrint?: (email: EmailLike) => void; + onDownload?: (email: EmailLike) => void; + onViewInWindow?: (email: EmailLike) => void; + onViewInRaw?: (email: EmailLike) => void; onSaveAsContact?: () => void; } @@ -77,47 +78,14 @@ export const EmailActions = ({ showDelete = true, handleSingleEmailMarkAsFlagged, handleSingleEmailMarkAsRead, - reFetchMails, onPrint, onDownload, onViewInWindow, onViewInRaw, onSaveAsContact, }: EmailActionsProps) => { - const isFlagged = email?.FLAGS.includes('\\Flagged'); - const isRead = email?.FLAGS.includes('\\Seen'); - - const ActionButton = ({ - icon: Icon, - label, - onClick, - variant = 'default', - }: { - icon: React.ElementType; - label: string; - onClick: (e: React.MouseEvent) => void; - variant?: 'default' | 'danger'; - }) => ( - - - - ); - - const Divider = () =>
; + const isFlagged = !!email?.FLAGS?.includes('\\Flagged'); + const isRead = !!email?.FLAGS?.includes('\\Seen'); const moreItems = [ { @@ -185,13 +153,13 @@ export const EmailActions = ({ key: 'mark-read', label: isRead ? 'Mark as Unread' : 'Mark as Read', icon: isRead ? MdMarkEmailUnread : MdMarkEmailRead, - onSelect: () => handleSingleEmailMarkAsRead?.(email?.id, !isRead), + onSelect: () => handleSingleEmailMarkAsRead?.(String(email?.id ?? ''), !isRead), }, { key: 'flag', label: isFlagged ? 'Remove Flag' : 'Add Flag', icon: isFlagged ? LuFlagOff : FaRegFlag, - onSelect: () => handleSingleEmailMarkAsFlagged?.(email?.id, isFlagged), + onSelect: () => handleSingleEmailMarkAsFlagged?.(String(email?.id ?? ''), isFlagged), }, // View / export group { key: 'sep-more', label: '', separator: true }, @@ -205,7 +173,7 @@ export const EmailActions = ({ label: 'Delete', icon: FaTrash, color: 'red', - onSelect: () => onDelete(email.id), + onSelect: () => onDelete(String(email.id)), }, ] : []), diff --git a/src/components/mailbox/EmailAttachments.tsx b/src/components/mailbox/EmailAttachments.tsx index b7fd59c..8995c95 100644 --- a/src/components/mailbox/EmailAttachments.tsx +++ b/src/components/mailbox/EmailAttachments.tsx @@ -15,7 +15,7 @@ * . */ -import { useState, useEffect, useCallback, useRef } from 'react'; +import { useState, useEffect, useCallback } from 'react'; import { FaPaperclip, FaDownload, @@ -40,22 +40,29 @@ import { FaExclamationTriangle, FaEnvelope, } from 'react-icons/fa'; -import { useToast } from '../ui/ToastComponent'; +import { useToast } from '../../hooks/useToast'; import { sanitizeHTMLContent } from '../../utils/sanitizeHTMLContent'; -import JSZip from 'jszip'; + +/** Loose shape covering both postal-mime attachments and the composer's own attachment payloads. */ +export interface EmailAttachment { + filename?: string; + mimeType: string; + content: string; + contentId?: string; +} interface EmailAttachmentsProps { - attachments: any[]; + attachments: EmailAttachment[]; emailHtml?: string; } -const getSafeExtension = (filename: string): string => { +const getSafeExtension = (filename?: string): string => { if (!filename) return ''; const parts = filename?.split('.'); return parts.length > 1 ? parts.pop()?.toLowerCase() || '' : ''; }; -const normalizeMimeType = (mimeType: string, filename: string): string => { +const normalizeMimeType = (mimeType: string, filename?: string): string => { if (mimeType && mimeType !== 'application/octet-stream') return mimeType; const ext = getSafeExtension(filename); @@ -141,7 +148,7 @@ const decodeRfc2047 = (str: string): string => { } else { const decoded = text .replace(/_/g, ' ') - .replace(/=([0-9A-Fa-f]{2})/g, (__: any, hex: any) => + .replace(/=([0-9A-Fa-f]{2})/g, (_match: string, hex: string) => String.fromCharCode(parseInt(hex, 16)) ); return decoded; @@ -544,193 +551,6 @@ ${result.value} `; }; -const renderPptxToHtml = async (base64Content: string): Promise => { - const base64Data = base64Content.replace(/^data:[^;]+;base64,/, ''); - const binaryStr = atob(base64Data); - const bytes = new Uint8Array(binaryStr.length); - for (let i = 0; i < binaryStr.length; i++) bytes[i] = binaryStr.charCodeAt(i); - - const zip = await JSZip.loadAsync(bytes.buffer); - - const slideFiles = Object.keys(zip.files) - .filter((name) => /^ppt\/slides\/slide\d+\.xml$/.test(name)) - .sort((a, b) => { - const na = parseInt(a.match(/slide(\d+)/)?.[1] ?? '0'); - const nb = parseInt(b.match(/slide(\d+)/)?.[1] ?? '0'); - return na - nb; - }); - - if (!slideFiles.length) throw new Error('No slides found in presentation.'); - - let accentColor = '#4f46e5'; - try { - const themeXml = await zip.files['ppt/theme/theme1.xml']?.async('string'); - if (themeXml) { - const accent = themeXml.match(/]*>.*? { - const parser = new DOMParser(); - const doc = parser.parseFromString(xml, 'text/xml'); - - const getTitleText = (el: Element): string => - Array.from(el.querySelectorAll('r t')) - .map((t) => t.textContent) - .join('') - .trim(); - - const titles: string[] = []; - const bullets: string[] = []; - - const titlePlaceholders = doc.querySelectorAll('sp'); - titlePlaceholders.forEach((sp) => { - const phType = sp.querySelector('ph')?.getAttribute('type') ?? ''; - const text = getTitleText(sp); - if (!text) return; - if (phType === 'ctrTitle' || phType === 'title' || phType === 'subTitle') { - titles.push(text); - } else { - const paras = sp.querySelectorAll('p'); - paras.forEach((p) => { - const paraText = Array.from(p.querySelectorAll('r t')) - .map((t) => t.textContent) - .join('') - .trim(); - const level = parseInt(p.querySelector('pPr')?.getAttribute('lvl') ?? '0'); - if (paraText) bullets.push(`${level}:::${paraText}`); - }); - } - }); - - return { titles, bullets, notes: '' }; - }; - - const slideHtmlParts: string[] = []; - - for (let i = 0; i < slideFiles.length; i++) { - const xml = await zip.files[slideFiles[i]].async('string'); - const { titles, bullets } = parseXmlText(xml); - - const titleHtml = titles - .map((t, idx) => - idx === 0 - ? `

${escHtml(t)}

` - : `

${escHtml(t)}

` - ) - .join(''); - - const bulletHtml = bullets - .map((b) => { - const [lvlStr, ...rest] = b.split(':::'); - const lvl = parseInt(lvlStr); - const text = rest.join(':::'); - return `
  • ${escHtml(text)}
  • `; - }) - .join(''); - - const isEmpty = !titles.length && !bullets.length; - - slideHtmlParts.push(` -
    -
    ${i + 1} / ${slideFiles.length}
    - ${ - isEmpty - ? '
    [ No text content on this slide ]
    ' - : `${titleHtml}${bulletHtml ? `
      ${bulletHtml}
    ` : ''}` - } -
    - `); - } - - return ` - - - - - - -
    - - 1 / ${slideFiles.length} - -
    -
    - ${slideHtmlParts.join('')} -
    -
    - ${slideHtmlParts - .map( - ( - _, - i - ) => `
    -
    Slide ${i + 1}
    -
    ` - ) - .join('')} -
    - - -`; -}; const escHtml = (str: string): string => str.replace(/&/g, '&').replace(//g, '>').replace(/"/g, '"'); @@ -837,7 +657,7 @@ const EmailAttachments = ({ attachments, emailHtml = '' }: EmailAttachmentsProps }, []); const base64ToFile = useCallback( - (attachment: any): File | null => { + (attachment: EmailAttachment): File | null => { const mime = normalizeMimeType(attachment.mimeType, attachment.filename); const blob = base64ToBlob(attachment.content, mime); if (!blob) return null; @@ -849,7 +669,7 @@ const EmailAttachments = ({ attachments, emailHtml = '' }: EmailAttachmentsProps // ─── Download / Copy / Share ─────────────────────────────────────────────── const handleDownload = useCallback( - (attachment: any) => { + (attachment: EmailAttachment) => { const mime = normalizeMimeType(attachment.mimeType, attachment.filename); const blob = base64ToBlob(attachment.content, mime); if (!blob) return; @@ -866,7 +686,7 @@ const EmailAttachments = ({ attachments, emailHtml = '' }: EmailAttachmentsProps ); const handleCopyToClipboard = useCallback( - async (attachment: any) => { + async (attachment: EmailAttachment) => { const file = base64ToFile(attachment); if (!file) { toast.error({ description: 'Failed to prepare file' }); @@ -879,11 +699,11 @@ const EmailAttachments = ({ attachments, emailHtml = '' }: EmailAttachmentsProps toast.error({ description: 'Failed to copy file' }); } }, - [base64ToFile] + [base64ToFile, toast] ); const handleShare = useCallback( - async (attachment: any) => { + async (attachment: EmailAttachment) => { const file = base64ToFile(attachment); if (!file) { toast.error({ description: 'Failed to prepare file' }); @@ -903,7 +723,7 @@ const EmailAttachments = ({ attachments, emailHtml = '' }: EmailAttachmentsProps handleDownload(attachment); toast.success({ description: `📁 Downloaded "${attachment.filename}"` }); }, - [base64ToFile, canShare, handleDownload] + [base64ToFile, canShare, handleDownload, toast] ); // ─── Preview capability ──────────────────────────────────────────────────── @@ -921,7 +741,7 @@ const EmailAttachments = ({ attachments, emailHtml = '' }: EmailAttachmentsProps // ─── Blob URL creation ───────────────────────────────────────────────────── const createBlobUrl = useCallback( - (attachment: any, index: number): string | null => { + (attachment: EmailAttachment, index: number): string | null => { if (blobUrls.has(index)) return blobUrls.get(index)!; const mime = normalizeMimeType(attachment.mimeType, attachment.filename); const blob = base64ToBlob(attachment.content, mime); @@ -936,7 +756,7 @@ const EmailAttachments = ({ attachments, emailHtml = '' }: EmailAttachmentsProps // ─── Office + EML HTML rendering ────────────────────────────────────────── const processOfficePreview = useCallback( - async (attachment: any, index: number) => { + async (attachment: EmailAttachment, index: number) => { if (officeHtml.has(index)) return; const mime = normalizeMimeType(attachment.mimeType, attachment.filename); @@ -956,8 +776,9 @@ const EmailAttachments = ({ attachments, emailHtml = '' }: EmailAttachmentsProps next.delete(index); return next; }); - } catch (e: any) { - setOfficeError((prev) => new Map(prev).set(index, e?.message || 'Preview failed')); + } catch (e) { + const message = e instanceof Error ? e.message : 'Preview failed'; + setOfficeError((prev) => new Map(prev).set(index, message)); } finally { setLoadingIndex(null); } @@ -1004,7 +825,7 @@ const EmailAttachments = ({ attachments, emailHtml = '' }: EmailAttachmentsProps // ─── Render preview content ──────────────────────────────────────────────── - const renderPreviewContent = (attachment: any, index: number) => { + const renderPreviewContent = (attachment: EmailAttachment, index: number) => { const mime = normalizeMimeType(attachment.mimeType, attachment.filename); // EML / Office files rendered as HTML diff --git a/src/components/mailbox/EmailCard.tsx b/src/components/mailbox/EmailCard.tsx index 6d82d24..d33fe70 100644 --- a/src/components/mailbox/EmailCard.tsx +++ b/src/components/mailbox/EmailCard.tsx @@ -16,7 +16,7 @@ */ import { useState, useRef, useEffect } from 'react'; -import { FaPaperclip, FaTrash, FaFlag, FaEnvelope } from 'react-icons/fa'; +import { FaPaperclip, FaTrash, FaFlag } from 'react-icons/fa'; import { MdDriveFileMoveOutline, MdMarkEmailRead, MdMarkEmailUnread } from 'react-icons/md'; import { decodeWords } from 'postal-mime'; import BIMIAvatar from '../common/BimiAvatar'; @@ -32,6 +32,7 @@ import { useQueryClient } from '@tanstack/react-query'; import { useEmailParser } from '../../hooks/useEmailParser'; import { isFolderThreadEnabled, shouldApplyThreading } from '../../utils/emailListUtils'; import type { SimplifiedEmail } from '../../utils/email'; +import type { EmailLike } from '../../utils/emailThreading'; interface EmailCardProps { email: SimplifiedEmail; @@ -96,7 +97,7 @@ const EmailCard = ({ const [showEmailTooltip, setShowEmailTooltip] = useState(false); const [tooltipPosition, setTooltipPosition] = useState<'bottom' | 'top'>('bottom'); const tooltipTriggerRef = useRef(null); - const longPressTimerRef = useRef(null); + const longPressTimerRef = useRef | null>(null); const touchStartTimeRef = useRef(0); const dragImageRef = useRef(null); const touchStartYRef = useRef(0); @@ -146,7 +147,7 @@ const EmailCard = ({ const toRecipients = parseRecipients(email.To); const ccRecipients = email.Cc ? parseRecipients(email.Cc) : []; - const bccRecipients: any[] = []; + const bccRecipients: ReturnType = []; // Status Checks const emailSeen = email?.FLAGS?.includes('\\Seen') || false; @@ -161,9 +162,10 @@ const EmailCard = ({ const isFolderThread = isFolderThreadEnabled(folderThreadView, folder, folder); const threadingActive = shouldApplyThreading(threadedView, isFolderThread, folder); - const isThread = !!(email as any)['Thread-View']; - const threadCount = ((email as any)['Thread-Emails-Count'] as number) || 1; - const threadUnreadCount = ((email as any)['Thread-Unread-Count'] as number) || 0; + const emailWithThreadFields = email as unknown as EmailLike; + const isThread = !!emailWithThreadFields['Thread-View']; + const threadCount = (emailWithThreadFields['Thread-Emails-Count'] as number) || 1; + const threadUnreadCount = (emailWithThreadFields['Thread-Unread-Count'] as number) || 0; const threadHasUnread = isThread && threadUnreadCount > 0; const showThreadPill = threadingActive && isThread && threadCount > 1; @@ -194,7 +196,7 @@ const EmailCard = ({ }, 300); return () => clearInterval(pollInterval); - }, [showEmailTooltip, prefetchedRawEmail]); + }, [showEmailTooltip, prefetchedRawEmail, email.id, folder, messageId, queryClient]); // Cleanup all timers on unmount useEffect(() => { @@ -397,7 +399,7 @@ const EmailCard = ({ if (!email.attachments || email.attachments.length === 0) return ''; const names = email.attachments .slice(0, 3) - .map((att:any) => att.filename) + .map((att) => att.filename) .join(', '); if (email.attachments.length > 3) return `${names} + ${email.attachments.length - 3} more`; return names; @@ -406,7 +408,7 @@ const EmailCard = ({ const getHoverAttachmentNames = () => { if (hoverParsedEmail?.attachments?.length) { return hoverParsedEmail.attachments - .map((a: any) => a.filename || a.name || 'Unnamed') + .map((a) => a.filename || (a as unknown as { name?: string }).name || 'Unnamed') .join(', '); } return getAttachmentNames(); @@ -561,7 +563,7 @@ const EmailCard = ({ longPressTimerRef.current = null; } if (touchDuration < 500 && !touchMovedRef.current && !isSelectionMode) - handleCardClick(e as any); + handleCardClick(e as unknown as React.MouseEvent); touchMovedRef.current = false; }; @@ -793,7 +795,7 @@ const EmailCard = ({ onSelectionChange?.(email.id, checked as boolean, index, false); }} onClick={(e) => { - if ((e as any).shiftKey && onSelectionChange) { + if (e.shiftKey && onSelectionChange) { e.stopPropagation(); onSelectionChange(email.id, !isSelected, index, true); } diff --git a/src/components/mailbox/EmailComposer.tsx b/src/components/mailbox/EmailComposer.tsx index 4f34b7a..02daeff 100644 --- a/src/components/mailbox/EmailComposer.tsx +++ b/src/components/mailbox/EmailComposer.tsx @@ -18,6 +18,7 @@ import { useState, useEffect, useCallback, useRef, useMemo } from 'react'; import { FaPaperPlane, FaDeleteLeft, FaFloppyDisk } from 'react-icons/fa6'; import type { Email } from '../../api/mailbox'; +import type { Email as ParsedPostalEmail, Address as PostalMimeAddress } from 'postal-mime'; import { useParams } from '@tanstack/react-router'; import { useEmailRaw } from '../../hooks/useEmailRaw'; import PostalMime, { decodeWords } from 'postal-mime'; @@ -33,18 +34,19 @@ import { emailAddress } from '../../state/emailAddress'; import RecipientField, { type RecipientFieldHandle } from '../composer/RecipientField'; import SubjectField from '../composer/SubjectField'; import ContentEditor from '../composer/contentEditor'; -import AttachmentUploader, { +import AttachmentUploader from '../composer/AttachmentUploader'; +import { toBase64, MAX_TOTAL_SIZE, MAX_INDIVIDUAL_FILE_SIZE, formatFileSize, -} from '../composer/AttachmentUploader'; +} from '../composer/attachmentUtils'; import { useDropzone } from 'react-dropzone'; import { FaPaperclip } from 'react-icons/fa6'; import EmailPriorityField from '../composer/EmailPriorityField'; -import { generateMessageId, sendMailV2 } from '../../api/composer'; +import { generateMessageId, sendMailV2, type ComposerRequest } from '../../api/composer'; import { useDeleteMail } from '../../hooks/useEmails'; -import { useToast } from '../ui/ToastComponent'; +import { useToast } from '../../hooks/useToast'; import { userSettingsAtom } from '../../state/settings'; import { folderQuotaAtom } from '../../state/folders'; @@ -58,16 +60,33 @@ import { processIncomingHtml, type EmailPriority, type EmailHeaders, + type Address, + type EmailAttachmentPayload, } from '../../utils/replyForwardHelper'; -import { FaTimes } from 'react-icons/fa'; import CustomModal from '../composer/CustomModal'; import { parseEmail } from '../../utils/emailPerser'; -import { escapeHtml } from '../../utils/emailPrint'; import { userDetailsAtom } from '../../state/userDetails'; import { SEND_DEFAULT } from '../../constants/constant'; import { getMessageId, normalizeFieldNames } from '../../utils/emailUtils'; import { getEditorDimensions } from '../../utils/dimensions'; import { useIsMobile } from '../../hooks/use-mobile'; +import type { ComposerEmail } from '../../state/emailComposer'; +import type { EmailAddress as RecipientEmailAddress } from '../../state/composer'; + +// Recipients built here (from parseEmailAddresses / RecipientField) use the +// {address, name} shape, not the {email, name} shape ComposerEmail declares for +// to/cc/bcc — a pre-existing mismatch, preserved as-is (see CLAUDE.md). +interface ComposerBasicData { + to: Address[]; + cc: Address[]; + bcc: Address[]; + subject: string; + html: string; + text: string; + attachments: EmailAttachmentPayload[]; + headers: EmailHeaders; + from_id: { email: string; name: string }; +} interface EmailComposerProps { email: Email | null; @@ -78,11 +97,14 @@ interface EmailComposerProps { } // Updated useDebounce hook with cancel function -const useDebounce = (callback: (...args: any[]) => void, delay: number) => { +const useDebounce = ( + callback: (...args: TArgs) => void, + delay: number +) => { const timeoutRef = useRef(null); const debouncedFunction = useCallback( - (...args: any[]) => { + (...args: TArgs) => { if (timeoutRef.current) { window.clearTimeout(timeoutRef.current); } @@ -101,7 +123,7 @@ const useDebounce = (callback: (...args: any[]) => void, delay: number) => { return [debouncedFunction, cancel] as const; }; -const EmailComposer = ({ email, mode, onClose, onSend, onSendDraft }: EmailComposerProps) => { +const EmailComposer = ({ email, mode, onClose, onSend }: EmailComposerProps) => { const toast = useToast(); const { folder } = useParams({ strict: false }); const [composerData, setComposerData] = useAtom(emailComposerDataAtom); @@ -139,7 +161,7 @@ const EmailComposer = ({ email, mode, onClose, onSend, onSendDraft }: EmailCompo // ── Undo-send state & refs ────────────────────────────────────────────────── const [undoTime, setUndoTime] = useState(5000); - const pendingEmailRef = useRef(null); + const pendingEmailRef = useRef | null>(null); const isUndoPeriodRef = useRef(false); const isSendInFlightRef = useRef(false); // ─────────────────────────────────────────────────────────────────────────── @@ -237,7 +259,7 @@ const EmailComposer = ({ email, mode, onClose, onSend, onSendDraft }: EmailCompo try { resetComposerData(); - const basicData: any = { + const basicData: ComposerBasicData = { to: [], cc: [], bcc: [], @@ -270,7 +292,10 @@ const EmailComposer = ({ email, mode, onClose, onSend, onSendDraft }: EmailCompo subject: decodeWords(email.Subject) || '', html: html, text: parsed.text || '', - attachments: regularAttachments, + // regularAttachments is backend/snake_case-shaped (mime_type, data, content_id), + // not the composer's own EmailAttachment shape — pre-existing mismatch, unrelated + // to this pass, preserved as-is. + attachments: regularAttachments as unknown as typeof prev.attachments, from_id: { email: currentEmail.address || '', name: currentEmail.name || '', @@ -282,8 +307,8 @@ const EmailComposer = ({ email, mode, onClose, onSend, onSendDraft }: EmailCompo return; } - let parsed: any = null; - let allAttachments: any[] = []; + let parsed: ParsedPostalEmail | null = null; + let allAttachments: EmailAttachmentPayload[] = []; if (rawEmail) { try { @@ -311,15 +336,22 @@ const EmailComposer = ({ email, mode, onClose, onSend, onSendDraft }: EmailCompo if (mode === 'draft') { if (parsed) { - const toAddressArray = (address: any): { name: string; address: string }[] => { + const toAddressArray = ( + address: PostalMimeAddress[] | PostalMimeAddress | undefined + ): Address[] => { if (!address) return []; if (Array.isArray(address)) { return address.map((addr) => ({ name: addr.name || '', - address: addr.address || '', + address: (addr as { address?: string }).address || '', })); } - return [{ name: address.name || '', address: address.address || '' }]; + return [ + { + name: address.name || '', + address: (address as { address?: string }).address || '', + }, + ]; }; basicData.to = toAddressArray(parsed.to); @@ -355,8 +387,8 @@ const EmailComposer = ({ email, mode, onClose, onSend, onSendDraft }: EmailCompo primaryRecipients = [parseEmailAddresses(email.From || '')[0]].filter(Boolean); } - const toAddresses = parseEmailAddresses(normalizedEmails?.to || ''); - const ccAddresses = parseEmailAddresses(normalizedEmails?.cc || ''); + const toAddresses = parseEmailAddresses((normalizedEmails?.to as string) || ''); + const ccAddresses = parseEmailAddresses((normalizedEmails?.cc as string) || ''); const currentAddress = typeof currentEmail === 'string' ? currentEmail : currentEmail?.address; @@ -383,7 +415,7 @@ const EmailComposer = ({ email, mode, onClose, onSend, onSendDraft }: EmailCompo basicData.to = []; if (parsed && parsed.html) { - const { html, regularAttachments } = processIncomingHtml(parsed.html, allAttachments); + const { regularAttachments } = processIncomingHtml(parsed.html, allAttachments); basicData.attachments = regularAttachments; } else if (allAttachments.length > 0) { basicData.attachments = allAttachments; @@ -395,16 +427,19 @@ const EmailComposer = ({ email, mode, onClose, onSend, onSendDraft }: EmailCompo name: currentEmail.name || '', }; - setComposerData((prev) => ({ - ...prev, - ...basicData, - })); + setComposerData( + (prev) => + ({ + ...prev, + ...basicData, + }) as unknown as ComposerEmail + ); if (mode !== 'draft' && mode !== 'new') { const originalDate = formatEmailDate(email.Date || ''); const originalFrom = parseEmail(email.From) || ''; - const originalTo = parseEmail(normalizedEmails.to) || ''; + const originalTo = parseEmail(normalizedEmails.to as string) || ''; const originalSubject = decodeWords(email.Subject) || ''; const borderColors = { @@ -517,6 +552,8 @@ const EmailComposer = ({ email, mode, onClose, onSend, onSendDraft }: EmailCompo userSettings, rawEmail, isLoadingRaw, + setComposerData, + resetComposerData, ]); const saveToDraft = useCallback(() => { @@ -534,17 +571,20 @@ const EmailComposer = ({ email, mode, onClose, onSend, onSendDraft }: EmailCompo folder_path: saveDraft || 'Drafts', priority, isDraft: true, - }) as any; + }); - draftMutate(mailData, { + // formatComposedEmailData returns html/text fields; ComposerRequest expects + // body_html/body_text. This autosave path (unlike handleMailAction) never + // renames them — a pre-existing mismatch, preserved as-is (see CLAUDE.md). + draftMutate(mailData as unknown as ComposerRequest, { onSuccess: () => { hasAutoSavedRef.current = true; }, - onError: (err: any) => { + onError: (err) => { console.error('error', err); }, }); - }, [composerData, quotedHtml, priority, draftMutate, isQuotaExceeded, saveDraft]); + }, [quotedHtml, priority, draftMutate, isQuotaExceeded, saveDraft]); const [debouncedSaveToDraft, cancelDebouncedSaveToDraft] = useDebounce(saveToDraft, 15000); @@ -563,7 +603,7 @@ const EmailComposer = ({ email, mode, onClose, onSend, onSendDraft }: EmailCompo errorTitle: string; setSending?: (value: boolean) => void; allowUndo?: boolean; - dataOverrides?: any; + dataOverrides?: Record; }) => { if (isDraft && isQuotaExceeded) { toast.error({ @@ -625,11 +665,11 @@ const EmailComposer = ({ email, mode, onClose, onSend, onSendDraft }: EmailCompo }, }; - const mailData = formatComposedEmailData(finalValue, { + const mailData: Record = formatComposedEmailData(finalValue, { folder_path: isDraft ? saveDraft || 'Drafts' : sendPath || folder_path, priority: priority, isDraft, - }) as any; + }); const mutateFn = isDraft ? draftMutate : sendMutate; @@ -659,7 +699,7 @@ const EmailComposer = ({ email, mode, onClose, onSend, onSendDraft }: EmailCompo : originalMessageId; if (draftMsgId) { mailData.draft_saved = true; - mailData.draft_folder_name = (email as any).folderPath || saveDraft || 'Drafts'; + mailData.draft_folder_name = email.folderPath || saveDraft || 'Drafts'; mailData.draft_message_id = draftMsgId; } } else if (hasAutoSavedRef.current) { @@ -730,7 +770,7 @@ const EmailComposer = ({ email, mode, onClose, onSend, onSendDraft }: EmailCompo isSendInFlightRef.current = true; const loadingId = toast.loading({ description: 'Sending…' }); try { - await sendMailV2(mailToSend); + await sendMailV2(mailToSend as unknown as ComposerRequest); isSendInFlightRef.current = false; toast.dismiss(loadingId); window.removeEventListener('beforeunload', handleBeforeUnload); @@ -744,12 +784,14 @@ const EmailComposer = ({ email, mode, onClose, onSend, onSendDraft }: EmailCompo hasAutoSavedRef.current = false; emailIdRef.current = null; setIsInitialized(false); - } catch (err: any) { + } catch (err) { isSendInFlightRef.current = false; toast.dismiss(loadingId); window.removeEventListener('beforeunload', handleBeforeUnload); toast.error({ - description: err?.message || 'An error occurred while sending mail. Please try again.', + description: + (err instanceof Error && err.message) || + 'An error occurred while sending mail. Please try again.', }); // Reopen the composer — data is still intact since the component stayed mounted setKeepMounted(false); @@ -761,7 +803,7 @@ const EmailComposer = ({ email, mode, onClose, onSend, onSendDraft }: EmailCompo } // ─────────────────────────────────────────────────────────────────────── - mutateFn(mailData, { + mutateFn(mailData as unknown as ComposerRequest, { onSuccess: () => { if (isDraft) { isManualSaveRef.current = false; @@ -779,7 +821,7 @@ const EmailComposer = ({ email, mode, onClose, onSend, onSendDraft }: EmailCompo emailIdRef.current = null; setIsInitialized(false); }, - onError: (err: any) => { + onError: (err) => { if (isDraft) { isManualSaveRef.current = false; } @@ -799,9 +841,21 @@ const EmailComposer = ({ email, mode, onClose, onSend, onSendDraft }: EmailCompo const pendingCc = ccRef.current?.flush(); const pendingBcc = bccRef.current?.flush(); - const effectiveTo: any = [...(composerData.to || []), ...(pendingTo ? [pendingTo] : [])]; - const effectiveCc: any = [...(composerData.cc || []), ...(pendingCc ? [pendingCc] : [])]; - const effectiveBcc: any = [...(composerData.bcc || []), ...(pendingBcc ? [pendingBcc] : [])]; + // composerData.to/cc/bcc are declared as {email, name} (ComposerEmail) but + // RecipientField actually stores/emits {address, name} at runtime — see the + // ComposerBasicData note above. Cast once here to the shape actually used below. + const effectiveTo: RecipientEmailAddress[] = [ + ...((composerData.to || []) as unknown as RecipientEmailAddress[]), + ...(pendingTo ? [pendingTo] : []), + ]; + const effectiveCc: RecipientEmailAddress[] = [ + ...((composerData.cc || []) as unknown as RecipientEmailAddress[]), + ...(pendingCc ? [pendingCc] : []), + ]; + const effectiveBcc: RecipientEmailAddress[] = [ + ...((composerData.bcc || []) as unknown as RecipientEmailAddress[]), + ...(pendingBcc ? [pendingBcc] : []), + ]; if (effectiveTo.length === 0) { toast.error({ description: 'Please add at least one recipient.' }); @@ -810,9 +864,9 @@ const EmailComposer = ({ email, mode, onClose, onSend, onSendDraft }: EmailCompo // ── Validate all recipient addresses ───────────────────────────────────── const allRecipients = [ - ...effectiveTo.map((r: any) => ({ ...r, field: 'To' })), - ...effectiveCc.map((r: any) => ({ ...r, field: 'Cc' })), - ...effectiveBcc.map((r: any) => ({ ...r, field: 'Bcc' })), + ...effectiveTo.map((r) => ({ ...r, field: 'To' })), + ...effectiveCc.map((r) => ({ ...r, field: 'Cc' })), + ...effectiveBcc.map((r) => ({ ...r, field: 'Bcc' })), ]; for (const recipient of allRecipients) { @@ -829,7 +883,7 @@ const EmailComposer = ({ email, mode, onClose, onSend, onSendDraft }: EmailCompo } // ───────────────────────────────────────────────────────────────────────── - const overrides: any = {}; + const overrides: Record = {}; if (pendingTo) overrides.to = effectiveTo; if (pendingCc) overrides.cc = effectiveCc; if (pendingBcc) overrides.bcc = effectiveBcc; @@ -853,7 +907,9 @@ const EmailComposer = ({ email, mode, onClose, onSend, onSendDraft }: EmailCompo const pendingBcc = bccRef.current?.flush(); // Calculate effective recipients for validation - const effectiveTo: any = [...(composerData.to || [])]; + const effectiveTo: RecipientEmailAddress[] = [ + ...((composerData.to || []) as unknown as RecipientEmailAddress[]), + ]; if (pendingTo) effectiveTo.push(pendingTo); if (isQuotaExceeded) { @@ -877,7 +933,7 @@ const EmailComposer = ({ email, mode, onClose, onSend, onSendDraft }: EmailCompo hasAutoSavedRef.current = true; - const overrides: any = {}; + const overrides: Record = {}; if (pendingTo) overrides.to = effectiveTo; if (pendingCc) overrides.cc = [...(composerData.cc || []), pendingCc]; if (pendingBcc) overrides.bcc = [...(composerData.bcc || []), pendingBcc]; @@ -908,10 +964,11 @@ const EmailComposer = ({ email, mode, onClose, onSend, onSendDraft }: EmailCompo setIsInitialized(false); }; - const updateComposerData = (fields: any) => { - setComposerData((prev) => ({ ...prev, ...fields })); + const updateComposerData = (fields: Record) => { + setComposerData((prev) => ({ ...prev, ...fields }) as unknown as ComposerEmail); - if (!isManualSaveRef.current && !isQuotaExceeded && fields?.text && fields.text.length > 5) { + const text = (fields as { text?: string }).text; + if (!isManualSaveRef.current && !isQuotaExceeded && text && text.length > 5) { debouncedSaveToDraft(); } }; @@ -919,14 +976,17 @@ const EmailComposer = ({ email, mode, onClose, onSend, onSendDraft }: EmailCompo useEffect(() => { if (mode === 'new' && !isInitialized) { setComposerData((prev) => { - const from: any = prev.from_id || {}; + const from = (prev.from_id || {}) as { address?: string; name?: string }; return { ...prev, + // ComposerEmail has no `from` field (only `from_id`) — this is a + // pre-existing dead assignment, nothing reads `composerData.from`. + // Preserved as-is (see CLAUDE.md). from: { address: from?.address?.trim() ? from.address : '', name: from.name?.trim() ? from.name : 'Unknown Sender', }, - }; + } as unknown as ComposerEmail; }); setIsInitialized(true); } @@ -951,7 +1011,10 @@ const EmailComposer = ({ email, mode, onClose, onSend, onSendDraft }: EmailCompo const onDrop = useCallback( async (acceptedFiles: File[]) => { const currentTotalSize = (composerData.attachments || []).reduce( - (sum: number, file: any) => sum + (file.size || 0), + // ComposerEmail declares attachments as {filename, mimeType, content}, but + // entries actually pushed below are {filename, mime_type, data, size} — + // a pre-existing shape mismatch, preserved as-is (see CLAUDE.md). + (sum: number, file) => sum + ((file as unknown as { size?: number }).size || 0), 0 ); const newFilesTotalSize = acceptedFiles.reduce((sum, file) => sum + file.size, 0); @@ -986,16 +1049,19 @@ const EmailComposer = ({ email, mode, onClose, onSend, onSendDraft }: EmailCompo })) ); - setComposerData((prev: any) => ({ - ...prev, - attachments: [...(prev.attachments || []), ...base64Attachments], - })); + setComposerData( + (prev) => + ({ + ...prev, + attachments: [...(prev.attachments || []), ...base64Attachments], + }) as unknown as ComposerEmail + ); toast.success({ description: `Added ${validFiles.length} file(s)`, duration: 3000, }); - } catch (error) { + } catch { toast.error({ description: 'Failed to process files.', }); @@ -1051,7 +1117,8 @@ const EmailComposer = ({ email, mode, onClose, onSend, onSendDraft }: EmailCompo onClick={handleSaveDraft} className={`flex items-center gap-2 px-3 py-2 rounded text-sm transition-colors ${ isQuotaExceeded || - (!composerData.to?.length && !(toRef.current as any)?.inputValue) || + (!composerData.to?.length && + !(toRef.current as unknown as { inputValue?: string })?.inputValue) || isDrafting ? 'opacity-50 cursor-not-allowed text-[var(--gray-9)]' : 'text-[var(--gray-11)] hover:text-[var(--gray-12)] hover:bg-[var(--gray-3)]' @@ -1156,7 +1223,7 @@ const EmailComposer = ({ email, mode, onClose, onSend, onSendDraft }: EmailCompo ref={toRef} label="To" placeholder="Recipients email address" - value={(composerData as any).to || []} + value={(composerData.to as unknown as RecipientEmailAddress[]) || []} onChange={(emailAddresses) => updateComposerData({ to: emailAddresses })} /> @@ -1180,7 +1247,7 @@ const EmailComposer = ({ email, mode, onClose, onSend, onSendDraft }: EmailCompo ref={ccRef} label="Cc" placeholder="Enter cc email address" - value={(composerData as any).cc || []} + value={(composerData.cc as unknown as RecipientEmailAddress[]) || []} onChange={(emailAddresses) => updateComposerData({ cc: emailAddresses })} /> )} @@ -1190,7 +1257,7 @@ const EmailComposer = ({ email, mode, onClose, onSend, onSendDraft }: EmailCompo ref={bccRef} label="Bcc" placeholder="Enter bcc email address" - value={(composerData as any).bcc || []} + value={(composerData.bcc as unknown as RecipientEmailAddress[]) || []} onChange={(emailAddresses) => updateComposerData({ bcc: emailAddresses })} /> )} diff --git a/src/components/mailbox/EmailEmptyState.tsx b/src/components/mailbox/EmailEmptyState.tsx index e092fc7..3bddb74 100644 --- a/src/components/mailbox/EmailEmptyState.tsx +++ b/src/components/mailbox/EmailEmptyState.tsx @@ -53,6 +53,7 @@ const EmailEmptyState = ({ folder = 'INBOX', error, onRetry }: EmailEmptyStatePr title: 'Your inbox is empty', description: 'All caught up! No new messages to display.', tip: 'New emails will appear here when they arrive.', + isError: false, }; } @@ -62,6 +63,7 @@ const EmailEmptyState = ({ folder = 'INBOX', error, onRetry }: EmailEmptyStatePr title: 'No sent emails', description: "You haven't sent any emails yet.", tip: 'Compose and send your first email to see it here.', + isError: false, }; } @@ -71,6 +73,7 @@ const EmailEmptyState = ({ folder = 'INBOX', error, onRetry }: EmailEmptyStatePr title: 'Trash is empty', description: 'No deleted emails in trash.', tip: 'Deleted emails will be stored here for 30 days.', + isError: false, }; } @@ -80,6 +83,7 @@ const EmailEmptyState = ({ folder = 'INBOX', error, onRetry }: EmailEmptyStatePr title: 'No draft emails', description: "You don't have any saved drafts.", tip: 'Start composing an email and save it as draft.', + isError: false, }; } @@ -89,6 +93,7 @@ const EmailEmptyState = ({ folder = 'INBOX', error, onRetry }: EmailEmptyStatePr title: 'No spam emails', description: 'Your spam folder is clean.', tip: 'Suspected spam emails will appear here.', + isError: false, }; } @@ -98,6 +103,7 @@ const EmailEmptyState = ({ folder = 'INBOX', error, onRetry }: EmailEmptyStatePr title: 'No archived emails', description: "You haven't archived any emails yet.", tip: 'Archive important emails to keep your inbox organized.', + isError: false, }; } @@ -107,6 +113,7 @@ const EmailEmptyState = ({ folder = 'INBOX', error, onRetry }: EmailEmptyStatePr title: 'Folder is empty', description: `No emails in ${folder}.`, tip: 'Move or filter emails to this folder to organize them.', + isError: false, }; }; @@ -120,7 +127,7 @@ const EmailEmptyState = ({ folder = 'INBOX', error, onRetry }: EmailEmptyStatePr
    {content.icon} @@ -134,7 +141,7 @@ const EmailEmptyState = ({ folder = 'INBOX', error, onRetry }: EmailEmptyStatePr
    {/* Retry Button (Only for errors) */} - {(content as any).isError && onRetry && ( + {content.isError && onRetry && (