From af0f47c0e36a3d3b4ffb6a1afed8a1a4824a1828 Mon Sep 17 00:00:00 2001 From: rustmailer Date: Sun, 15 Mar 2026 03:36:02 +0800 Subject: [PATCH] feat(search): integrate mailbox directory tree into search interface --- src/modules/imap/tests.rs | 13 +- web/package.json | 2 +- .../components/layout/data/sidebar-data.ts | 7 +- web/src/features/dashboard/index.tsx | 2 +- .../mailbox/components/account-switcher.tsx | 58 -- .../mailbox/components/bulk-actions.tsx | 173 ------ .../mailbox/components/delete-dialog.tsx | 109 ---- .../components/mail-display-drawer.tsx | 58 -- .../features/mailbox/components/mail-list.tsx | 230 -------- .../mailbox/components/mail-message-view.tsx | 350 ----------- web/src/features/mailbox/components/mail.tsx | 468 --------------- .../mailbox/components/mailbox-detail.tsx | 92 --- .../components/restore-message-dialog.tsx | 106 ---- .../mailbox/components/thread-dialog.tsx | 223 ------- web/src/features/mailbox/context/index.tsx | 63 -- web/src/features/mailbox/index.tsx | 46 -- .../search/account-mailbox-filter.tsx | 11 - web/src/features/search/bulk-actions.tsx | 17 +- web/src/features/search/context/index.tsx | 6 +- .../delete-mailbox-dialog.tsx | 6 +- web/src/features/search/index.tsx | 13 + web/src/features/search/mail-list.tsx | 2 +- web/src/features/search/mailbox-popover.tsx | 545 +++++++++++------- web/src/features/search/table/toolbar.tsx | 8 +- .../settings/api-tokens/token-list.tsx | 2 - web/src/routeTree.gen.ts | 32 - .../_authenticated/mailboxes/index.lazy.tsx | 25 - 27 files changed, 374 insertions(+), 2293 deletions(-) delete mode 100644 web/src/features/mailbox/components/account-switcher.tsx delete mode 100644 web/src/features/mailbox/components/bulk-actions.tsx delete mode 100644 web/src/features/mailbox/components/delete-dialog.tsx delete mode 100644 web/src/features/mailbox/components/mail-display-drawer.tsx delete mode 100644 web/src/features/mailbox/components/mail-list.tsx delete mode 100644 web/src/features/mailbox/components/mail-message-view.tsx delete mode 100644 web/src/features/mailbox/components/mail.tsx delete mode 100644 web/src/features/mailbox/components/mailbox-detail.tsx delete mode 100644 web/src/features/mailbox/components/restore-message-dialog.tsx delete mode 100644 web/src/features/mailbox/components/thread-dialog.tsx delete mode 100644 web/src/features/mailbox/context/index.tsx delete mode 100644 web/src/features/mailbox/index.tsx delete mode 100644 web/src/features/search/account-mailbox-filter.tsx rename web/src/features/{mailbox/components => search}/delete-mailbox-dialog.tsx (95%) delete mode 100644 web/src/routes/_authenticated/mailboxes/index.lazy.tsx diff --git a/src/modules/imap/tests.rs b/src/modules/imap/tests.rs index 0f52504..d4fd515 100644 --- a/src/modules/imap/tests.rs +++ b/src/modules/imap/tests.rs @@ -16,6 +16,8 @@ // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . +use std::io::Write; + use mail_parser::{parsers::MessageStream, MessageParser, MimeHeaders}; use crate::{ @@ -108,7 +110,7 @@ R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7 #[tokio::test] async fn test44() { - let path = r"C:\Users\polly\Downloads\test333.eml"; + let path = r"C:\Users\polly\Downloads\3462966311412541.eml"; let input = std::fs::read(path).unwrap(); let message = MessageParser::default().parse(&input).unwrap(); for attachment in message.attachments() { @@ -120,6 +122,15 @@ async fn test44() { let disposition = attachment.content_disposition(); + let body_start = attachment.raw_body_offset() as usize; + let body_end = attachment.raw_end_offset() as usize; + + if body_start < input.len() && body_end <= input.len() && body_start <= body_end { + //let raw_data = &input[body_start..body_end]; + let mut file = std::fs::File::create(&filename).unwrap(); + file.write_all(attachment.contents()).unwrap(); + } + let file_type = format!( "{}/{}", content_type.c_type.as_ref(), diff --git a/web/package.json b/web/package.json index cf70ab7..59d01d4 100644 --- a/web/package.json +++ b/web/package.json @@ -1,7 +1,7 @@ { "private": true, "name": "bichon-ui", - "version": "0.0.1", + "version": "1.0.0", "type": "module", "scripts": { "dev": "vite", diff --git a/web/src/components/layout/data/sidebar-data.ts b/web/src/components/layout/data/sidebar-data.ts index 37f00bf..53aa360 100644 --- a/web/src/components/layout/data/sidebar-data.ts +++ b/web/src/components/layout/data/sidebar-data.ts @@ -22,7 +22,7 @@ import { IconLayoutDashboard, IconSettings } from '@tabler/icons-react' -import { IdCard, Inbox, Mailbox, Search, Users2 } from 'lucide-react' +import { IdCard, Inbox, Search, Users2 } from 'lucide-react' import { type SidebarData } from '../types' import { useTranslation } from 'react-i18next' import { useCurrentUser } from '@/hooks/use-current-user' @@ -52,11 +52,6 @@ export function useSidebarData(): SidebarData { url: '/accounts', icon: Inbox, }, - { - title: t('navigation.mailbox'), - url: '/mailboxes', - icon: Mailbox, - }, { title: t('common.search'), url: '/search', diff --git a/web/src/features/dashboard/index.tsx b/web/src/features/dashboard/index.tsx index f1c1ee7..266af48 100644 --- a/web/src/features/dashboard/index.tsx +++ b/web/src/features/dashboard/index.tsx @@ -463,7 +463,7 @@ export default function MailArchiveDashboard() {
- © 2025 rustmailer.com - Bichon Email Archiving Project + © 2025-2026 rustmailer.com - Bichon Email Archiving Project
diff --git a/web/src/features/mailbox/components/account-switcher.tsx b/web/src/features/mailbox/components/account-switcher.tsx deleted file mode 100644 index b4a83c2..0000000 --- a/web/src/features/mailbox/components/account-switcher.tsx +++ /dev/null @@ -1,58 +0,0 @@ -// -// Copyright (c) 2025-2026 rustmailer.com (https://rustmailer.com) -// -// This file is part of the Bichon Email Archiving Project -// -// This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU Affero General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU Affero General Public License for more details. -// -// You should have received a copy of the GNU Affero General Public License -// along with this program. If not, see . - - - -import useMinimalAccountList from "@/hooks/use-minimal-account-list"; -import { VirtualizedSelect } from "@/components/virtualized-select"; -import { Button } from "@/components/ui/button"; -import { useNavigate } from "@tanstack/react-router"; -import { useTranslation } from "react-i18next"; - - -interface AccountSwitcherProps { - onAccountSelect: (accountId: number) => void, - defaultAccountId?: number, -} - -export function AccountSwitcher({ - onAccountSelect, - defaultAccountId -}: AccountSwitcherProps) { - const { accountsOptions, isLoading } = useMinimalAccountList(); - const navigate = useNavigate() - const { t } = useTranslation(); - if (isLoading) { - return
Loading...
; - } - - return ( - onAccountSelect(parseInt(values[0], 10))} - placeholder={t('oauth2.selectAnAccount')} - noItemsComponent={
-

No active email account.

- -
} - /> - ); -} \ No newline at end of file diff --git a/web/src/features/mailbox/components/bulk-actions.tsx b/web/src/features/mailbox/components/bulk-actions.tsx deleted file mode 100644 index 0d65152..0000000 --- a/web/src/features/mailbox/components/bulk-actions.tsx +++ /dev/null @@ -1,173 +0,0 @@ -// -// Copyright (c) 2025-2026 rustmailer.com (https://rustmailer.com) -// -// This file is part of the Bichon Email Archiving Project -// -// This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU Affero General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU Affero General Public License for more details. -// -// You should have received a copy of the GNU Affero General Public License -// along with this program. If not, see . - -import { useRef } from 'react'; -import { X, Trash2 } from 'lucide-react'; -import { cn } from '@/lib/utils'; -import { Badge } from '@/components/ui/badge'; -import { Button } from '@/components/ui/button'; -import { Separator } from '@/components/ui/separator'; -import { - Tooltip, - TooltipTrigger, - TooltipContent, -} from '@/components/ui/tooltip'; -import { useMailboxContext } from '../context'; -import { useTranslation } from 'react-i18next'; - -type MailBulkActionsProps = { - children?: React.ReactNode; -}; - -export function MailBulkActions({ children }: MailBulkActionsProps) { - const { t } = useTranslation(); - const { selected, setSelected, setOpen, setDeleteIds } = useMailboxContext(); - const toolbarRef = useRef(null); - const selectedCount = selected.size; - - const handleClearSelection = () => { - setSelected(new Set()); - }; - - const handleDelete = () => { - setDeleteIds(new Set(selected)); - setSelected(new Set()); - setOpen('move-to-trash'); - }; - - const handleKeyDown = (e: React.KeyboardEvent) => { - const buttons = toolbarRef.current?.querySelectorAll('button'); - if (!buttons || buttons.length === 0) return; - - const currentIndex = Array.from(buttons).findIndex( - (btn) => btn === document.activeElement - ); - - switch (e.key) { - case 'ArrowRight': { - e.preventDefault(); - const next = (currentIndex + 1) % buttons.length; - buttons[next]?.focus(); - break; - } - case 'ArrowLeft': { - e.preventDefault(); - const prev = currentIndex === 0 ? buttons.length - 1 : currentIndex - 1; - buttons[prev]?.focus(); - break; - } - case 'Home': - e.preventDefault(); - buttons[0]?.focus(); - break; - case 'End': - e.preventDefault(); - buttons[buttons.length - 1]?.focus(); - break; - case 'Escape': { - const target = e.target as HTMLElement; - const active = document.activeElement as HTMLElement; - const isFromDropdown = - target.closest('[data-slot="dropdown-menu-trigger"]') || - active.closest('[data-slot="dropdown-menu-trigger"]') || - target.closest('[data-slot="dropdown-menu-content"]') || - active.closest('[data-slot="dropdown-menu-content"]'); - - if (!isFromDropdown) { - e.preventDefault(); - handleClearSelection(); - } - break; - } - } - }; - - if (selectedCount === 0) return null; - - return ( - <> -
1 ? t('mailbox.bulkActions.emails') : t('mailbox.bulkActions.email') - })} - tabIndex={-1} - onKeyDown={handleKeyDown} - className={cn( - 'fixed bottom-6 left-1/2 z-50 -translate-x-1/2 rounded-xl', - 'transition-all delay-100 duration-300 ease-out hover:scale-105', - 'focus-visible:ring-ring/50 focus-visible:ring-2 focus-visible:outline-none' - )} - > -
- - - - - {t('mailbox.bulkActions.clearSelectionWithKey', { key: 'Escape' })} - - - -
- - {selectedCount} - {' '} - - {selectedCount > 1 ? t('mailbox.bulkActions.emails') : t('mailbox.bulkActions.email')} - {' '} - {t('mailbox.bulkActions.selected')} -
- - - - - - - {t('mailbox.bulkActions.deleteTooltip')} - - {children} -
-
- - ); -} diff --git a/web/src/features/mailbox/components/delete-dialog.tsx b/web/src/features/mailbox/components/delete-dialog.tsx deleted file mode 100644 index 1e2c5b9..0000000 --- a/web/src/features/mailbox/components/delete-dialog.tsx +++ /dev/null @@ -1,109 +0,0 @@ -// -// Copyright (c) 2025-2026 rustmailer.com (https://rustmailer.com) -// -// This file is part of the Bichon Email Archiving Project -// -// This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU Affero General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU Affero General Public License for more details. -// -// You should have received a copy of the GNU Affero General Public License -// along with this program. If not, see . - -import { IconAlertTriangle } from '@tabler/icons-react'; -import { toast } from '@/hooks/use-toast'; -import { Alert, AlertDescription, AlertTitle } from '@/components/ui/alert'; -import { ConfirmDialog } from '@/components/confirm-dialog'; -import { useMutation, useQueryClient } from '@tanstack/react-query'; -import { delete_messages } from '@/api/mailbox/envelope/api'; -import { useMailboxContext } from '../context'; -import { mapToRecordOfArrays } from '@/lib/utils'; -import { useTranslation } from 'react-i18next'; - -interface Props { - open: boolean; - onOpenChange: (open: boolean) => void; -} - -export function EnvelopeDeleteDialog({ open, onOpenChange }: Props) { - const queryClient = useQueryClient(); - const { selectedAccountId, deleteIds, setDeleteIds } = useMailboxContext(); - const { t } = useTranslation(); - - const deleteMutation = useMutation({ - mutationFn: ({ payload }: { payload: Record }) => delete_messages(payload), - retry: false, - onSuccess: () => { - queryClient.invalidateQueries({ queryKey: ['mailbox-list-messages'] }); - onOpenChange(false); - setDeleteIds(new Set()); - toast({ - title: t('mailbox.deleteDialog.successTitle'), - description: t('mailbox.deleteDialog.successDesc'), - }); - }, - onError: (error: any) => { - toast({ - title: t('mailbox.deleteDialog.errorTitle'), - description: `${error.message}`, - variant: 'destructive', - }); - }, - }); - - const handleDelete = () => { - if (selectedAccountId) { - const body = new Map>(); - body.set(selectedAccountId, deleteIds); - const payload = mapToRecordOfArrays(body); - deleteMutation.mutate({ payload }); - } - }; - - const isLoading = deleteMutation.isPending; - - const emailCount = deleteIds.size; - const countText = - emailCount > 1 - ? t('mailbox.deleteDialog.descCountMultiple', { count: emailCount }) - : t('mailbox.deleteDialog.descCountSingle'); - - return ( - - {' '} - {t('mailbox.deleteDialog.title')} - - } - desc={ -
-

- {t('mailbox.deleteDialog.desc', { countText })} -

- - - {t('mailbox.deleteDialog.warningTitle')} - {t('mailbox.deleteDialog.warningDesc')} - -
- } - confirmText={t('mailbox.deleteDialog.confirm')} - destructive - /> - ); -} diff --git a/web/src/features/mailbox/components/mail-display-drawer.tsx b/web/src/features/mailbox/components/mail-display-drawer.tsx deleted file mode 100644 index de030db..0000000 --- a/web/src/features/mailbox/components/mail-display-drawer.tsx +++ /dev/null @@ -1,58 +0,0 @@ -// -// Copyright (c) 2025-2026 rustmailer.com (https://rustmailer.com) -// -// This file is part of the Bichon Email Archiving Project -// -// This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU Affero General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU Affero General Public License for more details. -// -// You should have received a copy of the GNU Affero General Public License -// along with this program. If not, see . - - - -import { - Sheet, - SheetContent, - SheetTitle -} from '@/components/ui/sheet' -import { useMailboxContext } from '../context' -import { MailMessageView } from './mail-message-view' -import { VisuallyHidden } from '@radix-ui/react-visually-hidden' -import { useTranslation } from 'react-i18next' - - -interface Props { - open: boolean - onOpenChange: (open: boolean) => void -} - -export function MailDisplayDrawer({ open, onOpenChange }: Props) { - const { currentEnvelope } = useMailboxContext(); - const { t } = useTranslation() - return ( - - - - - - -
- {currentEnvelope ? ( - - ) : ( -
{t('mail.noMessageSelected')}
- )} -
- -
-
- ); -} diff --git a/web/src/features/mailbox/components/mail-list.tsx b/web/src/features/mailbox/components/mail-list.tsx deleted file mode 100644 index fc2247c..0000000 --- a/web/src/features/mailbox/components/mail-list.tsx +++ /dev/null @@ -1,230 +0,0 @@ -// -// Copyright (c) 2025-2026 rustmailer.com (https://rustmailer.com) -// -// This file is part of the Bichon Email Archiving Project -// -// This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU Affero General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU Affero General Public License for more details. -// -// You should have received a copy of the GNU Affero General Public License -// along with this program. If not, see . - - -import { cn, dateFnsLocaleMap, formatBytes } from "@/lib/utils" -import { formatDistanceToNow } from "date-fns" -import { MailIcon, MoreVertical, Paperclip, TagIcon, Trash2 } from "lucide-react" -import { Skeleton } from "@/components/ui/skeleton" -import { EmailEnvelope } from "@/api" -import { useMailboxContext } from "../context" -import { Checkbox } from "@/components/ui/checkbox" -import { MailBulkActions } from "./bulk-actions" -import { Badge } from "@/components/ui/badge" -import { useTranslation } from 'react-i18next' -import { enUS } from "date-fns/locale" -import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from "@/components/ui/dropdown-menu" -import { Button } from "@/components/ui/button" - -interface MailListProps { - items: EmailEnvelope[] - isLoading: boolean -} - -export function MailList({ - items, - isLoading, -}: MailListProps) { - const { t, i18n } = useTranslation() - const { currentEnvelope, setCurrentEnvelope, setDeleteIds, setOpen, selected, setSelected } = useMailboxContext() - const locale = dateFnsLocaleMap[i18n.language.toLowerCase()] ?? enUS; - - const handleDelete = (envelope: EmailEnvelope) => { - setDeleteIds(new Set([envelope.id])) - setOpen("move-to-trash") - } - - const totalSelected = selected.size; - - const handleToggleAll = () => { - const total = selected.size; - if (total === items.length && items.length > 0) { - setSelected(new Set()); - } else { - const set = new Set(); - for (const item of items) { - set.add(item.id); - } - setSelected(set); - } - } - - const hasSelected = (mailId: number) => { - return selected.has(mailId); - } - - const toggleSelected = (id: number) => { - setSelected(prev => { - const next = new Set(prev) - if (next.has(id)) { - next.delete(id) - } else { - next.add(id) - } - return next - }); - } - - if (isLoading) { - return ( -
- {Array.from({ length: 8 }).map((_, i) => ( -
- - - -
- ))} -
- ) - } - - return ( -
- {items.length > 0 && ( -
- 0 - ? true - : selected.size > 0 - ? "indeterminate" - : false - } - onCheckedChange={handleToggleAll} - className="h-4 w-4" - /> - - {selected.size > 0 - ? `${t('search.bulkActions.selected', { count: selected.size })}` - : t('common.selectAll')} - -
- )} - - {items.map((item, index) => { - const hasAttachments = item.attachment_count > 0 - const isSelected = currentEnvelope?.id === item.id - const isChecked = hasSelected(item.id) - return ( -
{ - const target = e.target as HTMLElement - if (target.closest('input[type="checkbox"], button')) return - setCurrentEnvelope(item); - setOpen("display") - }} - > - toggleSelected(item.id)} - onClick={(e) => e.stopPropagation()} - className="h-4 w-4 shrink-0" - /> - -
-
-
-

{item.from}

-

- {item.subject} -

-
-

- {item.subject} -

- -
- {item.tags?.map((tag, i) => ( - - {tag} - - ))} -
-
- -
- {hasAttachments && ( -
- - {item.attachment_count} -
- )} - {formatBytes(item.size)} - - {item.date && formatDistanceToNow(new Date(item.date), { addSuffix: true, locale })} - - - - - - - - - - e.stopPropagation()} - onSelect={(e) => { - e.stopPropagation(); - setSelected(new Set([item.id])); - setOpen("restore"); - }} - > - - {t('restore_message.restore_to_imap', 'Restore Mail')} - - e.stopPropagation()} - onSelect={(e) => { - e.stopPropagation(); - handleDelete(item); - }} - > - - {t('common.delete')} - - - -
-
-
- ) - })} - {totalSelected > 0 && } -
- ) -} diff --git a/web/src/features/mailbox/components/mail-message-view.tsx b/web/src/features/mailbox/components/mail-message-view.tsx deleted file mode 100644 index 8cad1e7..0000000 --- a/web/src/features/mailbox/components/mail-message-view.tsx +++ /dev/null @@ -1,350 +0,0 @@ -// -// Copyright (c) 2025-2026 rustmailer.com (https://rustmailer.com) -// -// This file is part of the Bichon Email Archiving Project -// -// This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU Affero General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU Affero General Public License for more details. -// -// You should have received a copy of the GNU Affero General Public License -// along with this program. If not, see . - - -import { useEffect, useState } from 'react'; -import { useMutation } from '@tanstack/react-query'; -import { Loader, Download, Trash2, MessageSquareMore, FileText, FileImage, FileAudio, FileVideo, FileSpreadsheet, FileArchive, FileCode, FileIcon } from 'lucide-react'; - -import { Button } from '@/components/ui/button'; -import { Separator } from '@/components/ui/separator'; -import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip'; -import { toast } from '@/hooks/use-toast'; -import { formatBytes, formatTimestamp } from '@/lib/utils'; -import { useMailboxContext } from '../context'; -import EmailIframe from '@/components/mail-iframe'; -import { - AttachmentInfo, - download_attachment, - download_message, - getContent, - load_message, -} from '@/api/mailbox/envelope/api'; -import { AxiosError } from 'axios'; -import { MailThreadDialog } from './thread-dialog'; -import { useTranslation } from 'react-i18next'; -import useMinimalAccountList from '@/hooks/use-minimal-account-list'; - -interface MailMessageViewProps { - envelope: { - account_id: number; - id: number; - from?: string; - to?: string[]; - cc?: string[]; - bcc?: string[]; - subject?: string; - internal_date?: number; - }; - showActions?: boolean; - showHeader?: boolean; - showAttachments?: boolean; -} - -const Multilines: React.FC<{ title: string; lines: string[] }> = ({ title, lines }) => { - const { t } = useTranslation() - const [expanded, setExpanded] = useState(false); - return ( -
-
- {title}: -
-
    - {lines.slice(0, expanded ? lines.length : 3).map((ref, i) => ( -
  • {ref}
  • - ))} -
- {lines.length > 3 && ( - - )} -
-
-
- ); -}; - - -const getFileConfig = (mimeType: string) => { - const type = mimeType.toLowerCase(); - if (type.includes('pdf')) { - return { icon: , color: 'text-red-600 bg-red-50 border-red-100' }; - } - if (type.includes('image/')) { - return { icon: , color: 'text-blue-600 bg-blue-50 border-blue-100' }; - } - if (type.includes('audio/')) { - return { icon: , color: 'text-purple-600 bg-purple-50 border-purple-100' }; - } - - if (type.includes('video/')) { - return { icon: , color: 'text-indigo-600 bg-indigo-50 border-indigo-100' }; - } - if (type.includes('spreadsheet') || type.includes('excel') || type.includes('csv')) { - return { icon: , color: 'text-green-600 bg-green-50 border-green-100' }; - } - if (type.includes('zip') || type.includes('compressed') || type.includes('archive')) { - return { icon: , color: 'text-orange-600 bg-orange-50 border-orange-100' }; - } - if (type.includes('text/') || type.includes('json') || type.includes('javascript')) { - return { icon: , color: 'text-slate-600 bg-slate-50 border-slate-100' }; - } - - return { icon: , color: 'text-gray-600 bg-gray-50 border-gray-100' }; -}; - -export function MailMessageView({ - envelope, - showActions = true, - showAttachments = true, - showHeader = true -}: MailMessageViewProps) { - const { t } = useTranslation() - const { selectedAccountId, setDeleteIds, setOpen } = useMailboxContext(); - - const [content, setContent] = useState(null); - const [contentType, setContentType] = useState<'Plain' | 'Html' | null>(null); - const [attachments, setAttachments] = useState(null); - const [loading, setLoading] = useState(true); - const [downloadingAttachmentFileName, setDownloadingAttachmentFileName] = useState(null); - - const { getEmailById } = useMinimalAccountList(); - const [threadOpen, setThreadOpen] = useState(false); - - const downloadAttachmentMutation = useMutation({ - mutationFn: ({ fileName }: { fileName: string }) => - download_attachment(selectedAccountId!, envelope.id, fileName), - onSuccess: () => setDownloadingAttachmentFileName(null), - onError: (error: any) => { - setDownloadingAttachmentFileName(null); - toast({ - title: t('mail.failedToDownloadFile'), - description: error.message, - variant: 'destructive', - }); - }, - }); - - const loadMessageMutation = useMutation({ - mutationFn: () => load_message(selectedAccountId!, envelope.id), - onSuccess: (data) => { - setLoading(false); - setContent(getContent(data)); - if (data.attachments) setAttachments(data.attachments); - setContentType(data.html ? 'Html' : 'Plain'); - }, - onError: (error: any) => { - setLoading(false); - toast({ - title: t('mail.failedToLoadEmail'), - description: error.message, - variant: 'destructive', - }); - }, - }); - - useEffect(() => { - setLoading(true); - loadMessageMutation.mutate(); - }, [envelope.id]); - - const handleDelete = () => { - setDeleteIds(new Set([envelope.id])); - setOpen('move-to-trash'); - }; - - const downloadEmlFile = async () => { - try { - toast({ title: t('mail.downloadStarted'), description: t('mail.isBeingDownloaded', { id: envelope.id }) }); - await download_message(selectedAccountId!, envelope.id); - toast({ title: t('mail.downloadComplete'), description: t('mail.downloaded', { id: envelope.id }) }); - } catch (error) { - let msg = t('mail.downloadFailed'); - if (error instanceof AxiosError) { - msg = error.response?.data?.message || error.response?.data?.error || error.message; - if (error.response?.status) msg = `${error.response.status}: ${msg}`; - } else if (error instanceof Error) { - msg = error.message; - } - toast({ title: t('mail.downloadFailed'), description: msg, variant: 'destructive' }); - } - }; - - return ( -
- {/* Header Info */} - {showHeader &&
-
- {t('mail.account')}: - {getEmailById(envelope.account_id)} -
-
- {t('mail.id')}: - {envelope.id} -
- {envelope.from && ( -
- {t('mail.from')}: - {envelope.from} -
- )} - {envelope.to && envelope.to.length > 0 && } - {envelope.cc && envelope.cc.length > 0 && } - {envelope.bcc && envelope.bcc.length > 0 && } - {envelope.subject && ( -
- {t('mail.subject')}: - {envelope.subject} -
- )} - {envelope.internal_date && ( -
- {t('mail.date')}: - {formatTimestamp(envelope.internal_date)} -
- )} -
} - {/* Action Bar */} - {showActions && ( - <> -
- -
-
- - - - - {t('mail.delete')} - - - - - - - {t('mail.download')} - - - - - - - {t('mail.viewThread')} - -
- - )} - {showAttachments && } - {/* Attachments */} - {showAttachments && ( -
- {loading ? ( - - ) : attachments && attachments.length > 0 ? ( - (() => { - const nonInline = attachments.filter((a) => !a.inline); - return nonInline.length > 0 ? ( -
- {nonInline.map((attachment, i) => { - const { icon, color } = getFileConfig(attachment.file_type); - return
-
-
- {icon} -
-
- - {attachment.filename} - - - {attachment.file_type.split('/').pop()?.toUpperCase()} - -
-
-
- - {formatBytes(attachment.size)} - - {downloadingAttachmentFileName === attachment.filename ? ( - - ) : ( - { - setDownloadingAttachmentFileName(attachment.filename); - downloadAttachmentMutation.mutate({ fileName: attachment.filename }); - }} - /> - )} -
-
- })} -
- ) : ( - - {t('mail.onlyNonInlineAttachments')} - - ); - })() - ) : ( - {t('mail.noAttachments')} - )} -
- )} - {showAttachments && } - {/* Content */} -
- {loading ? ( -
- - loading... -
- ) : content ? ( -
- {contentType === 'Html' ? ( - - ) : ( -
{content}
- )} -
- ) : ( -
No content available
- )} -
- - -
- ); -} diff --git a/web/src/features/mailbox/components/mail.tsx b/web/src/features/mailbox/components/mail.tsx deleted file mode 100644 index 525da67..0000000 --- a/web/src/features/mailbox/components/mail.tsx +++ /dev/null @@ -1,468 +0,0 @@ -// -// Copyright (c) 2025-2026 rustmailer.com (https://rustmailer.com) -// -// This file is part of the Bichon Email Archiving Project -// -// This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU Affero General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU Affero General Public License for more details. -// -// You should have received a copy of the GNU Affero General Public License -// along with this program. If not, see . - - -import * as React from "react" -import { cn } from "@/lib/utils" -import { - ResizableHandle, - ResizablePanel, - ResizablePanelGroup, -} from "@/components/ui/resizable" -import { Separator } from "@/components/ui/separator" -import { TooltipProvider } from "@/components/ui/tooltip" -import { AccountSwitcher } from "./account-switcher" -import { ScrollArea } from "@/components/ui/scroll-area" -import { list_mailboxes, MailboxData } from "@/api/mailbox/api" -import { useQuery } from "@tanstack/react-query" -import { Skeleton } from "@/components/ui/skeleton" -import MailboxProvider, { MailboxDialogType } from "../context" -import useDialogState from "@/hooks/use-dialog-state" -import { MailboxDialog } from "./mailbox-detail" -import { MailList } from "./mail-list" -import { list_messages } from "@/api/mailbox/envelope/api" -import { MailDisplayDrawer } from "./mail-display-drawer" -import { toast } from "@/hooks/use-toast" -import { EnvelopeDeleteDialog } from "./delete-dialog" -import Logo from '@/assets/logo.svg' -import { EmailEnvelope } from "@/api" -import { EnvelopeListPagination } from "@/components/pagination" -import { RichTreeView, TreeItemCheckbox, TreeItemContent, TreeItemDragAndDropOverlay, TreeItemIcon, TreeItemIconContainer, TreeItemLabel, TreeItemProvider, TreeItemRoot, useTreeItem, useTreeItemModel, UseTreeItemParameters } from "@mui/x-tree-view" -import { buildTree, ExtendedTreeItemProps } from "@/lib/build-tree" -import { useTheme } from "@/context/theme-context" -import { styled } from "@mui/material/styles" -import { animated, useSpring } from "@react-spring/web" -import { TransitionProps } from "@mui/material/transitions" -import Collapse from "@mui/material/Collapse" -import { FolderIcon, MoreVertical, Trash2 } from "lucide-react" -import { RestoreMessageDialog } from "./restore-message-dialog" -import { Button } from "@/components/ui/button" -import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from "@/components/ui/dropdown-menu" -import { useTranslation } from "react-i18next" -import { MailBoxDeleteDialog } from "./delete-mailbox-dialog" - - -interface MailProps { - defaultLayout: number[] | undefined - defaultCollapsed?: boolean - navCollapsedSize: number, - lastSelectedAccountId?: number | undefined -} - -interface ListMessagesOptions { - accountId: number | undefined; - mailboxId: number | undefined; - page: number; - page_size: number; -} - -const useListMessages = ({ accountId, mailboxId, page, page_size }: ListMessagesOptions) => { - return useQuery({ - queryKey: ['mailbox-list-messages', `${accountId}`, mailboxId, page, page_size], - queryFn: () => { - return list_messages(accountId!, mailboxId!, page, page_size); - }, - enabled: !!accountId && !!mailboxId, - retry: 0, - staleTime: 1000, - }); -}; - -interface CustomLabelProps { - exists?: number; - attributes?: { attr: string; extension: string | null }[], - children: React.ReactNode; - id: string; - icon?: React.ElementType; - expandable?: boolean; - onDelete: (id: string) => void; -} - -function CustomLabel({ - expandable, - exists, - attributes, - children, - id, - onDelete, - ...other -}: CustomLabelProps) { - const { t } = useTranslation() - return ( - - - - {children} - -
- - - - - - { - e.stopPropagation(); - }} - onSelect={(e) => { - e.preventDefault(); - onDelete(id); - }} - > - - {t('common.delete')} - - - -
-
- ); -} - -const CustomCollapse = styled(Collapse)({ - padding: 0, -}); - -const AnimatedCollapse = animated(CustomCollapse); - -function TransitionComponent(props: TransitionProps) { - const style = useSpring({ - to: { - opacity: props.in ? 1 : 0, - transform: `translate3d(0,${props.in ? 0 : 20}px,0)`, - }, - }); - - return ; -} - -interface CustomTreeItemProps - extends Omit, - Omit, 'onFocus'> { } - - - -export function Mail({ - defaultLayout = [20, 80], - defaultCollapsed = false, - navCollapsedSize, - lastSelectedAccountId, -}: MailProps) { - const [open, setOpen] = useDialogState(null) - const [isCollapsed, setIsCollapsed] = React.useState(defaultCollapsed) - const [selectedMailbox, setSelectedMailbox] = React.useState(undefined); - const [selectedAccountId, setSelectedAccountId] = React.useState(lastSelectedAccountId); - const [selectedEvelope, setSelectedEvelope] = React.useState(undefined); - const [page, setPage] = React.useState(0); - const [pageSize, setPageSize] = React.useState(30); - const [deleteIds, setDeleteIds] = React.useState>(() => new Set()); - const [selected, setSelected] = React.useState>(() => new Set()); - const [deleteMailboxId, setDeleteMailboxId] = React.useState(undefined); - - const { theme } = useTheme() - - const { data: mailboxes, isLoading: isMailboxesLoading } = useQuery({ - queryKey: ['account-mailboxes', `${selectedAccountId}`], - queryFn: () => list_mailboxes(selectedAccountId!, false), - enabled: !!selectedAccountId, - }) - - - const tree = buildTree(mailboxes ?? []); - - const { data: envelopes, isLoading: isMessagesLoading, isError, error } = useListMessages({ - accountId: selectedAccountId, - mailboxId: selectedMailbox?.id, - page: page + 1, - page_size: pageSize - }); - - const hasNextPage = () => { - return page + 1 < envelopes?.total_pages!; - } - - const handlePageChange = (newPage: number) => { - setPage(newPage); - } - - - const handlePageSizeChange = (newSize: number) => { - setPage(0); - setPageSize(newSize); - } - - React.useEffect(() => { - if (isError && error) { - toast({ - variant: "destructive", - title: "Failed to load messages", - description: error.message || "An unknown error occurred. Please try again.", - }); - } - }, [isError, error]); - - // const handleItemSelectionToggle = ( - // _event: React.SyntheticEvent | null, - // itemId: string, - // isSelected: boolean, - // ) => { - // if (isSelected) { - // setSelectedMailbox(mailboxes?.find(m => String(m.id) === itemId)) - // setPage(0); - // } - // }; - - const handleItemClick = ( - _event: React.SyntheticEvent | null, - itemId: string - ) => { - //console.log(itemId) - setSelectedMailbox(mailboxes?.find(m => String(m.id) === itemId)) - setPage(0); - }; - - const handleDeleteClick = (id: string) => { - setDeleteMailboxId(id); - setOpen('delete'); - }; - - const CustomTreeItem = React.useMemo(() => { - return React.forwardRef(function CustomTreeItem( - props: CustomTreeItemProps, - ref: React.Ref, - ) { - const { id, itemId, label, disabled, children, ...other } = props; - - const { - getContextProviderProps, - getRootProps, - getContentProps, - getIconContainerProps, - getCheckboxProps, - getLabelProps, - getGroupTransitionProps, - getDragAndDropOverlayProps, - status, - } = useTreeItem({ id, itemId, children, label, disabled, rootRef: ref }); - - const item = useTreeItemModel(itemId)!; - - return ( - - - - - - - - - - - {children && } - - - ); - }); - }, [theme]); - - return ( - - - { - localStorage.setItem('react-resizable-panels:layout:mail', JSON.stringify(sizes)); - }} - className="items-stretch" - > - { - setIsCollapsed(true); - localStorage.setItem('react-resizable-panels:collapsed', JSON.stringify(true)); - }} - onResize={() => { - setIsCollapsed(false); - localStorage.setItem('react-resizable-panels:collapsed', JSON.stringify(false)); - }} - className={cn( - isCollapsed && - "min-w-[50px] transition-all duration-300 ease-in-out" - )} - > - - -
- { - localStorage.setItem('mailbox:selectedAccountId', `${accountId}`); - setSelectedAccountId(accountId); - setSelectedMailbox(undefined); - }} defaultAccountId={lastSelectedAccountId} /> -
- - {isMailboxesLoading ? ( -
- {Array.from({ length: 5 }).map((_, index) => ( -
-
- - -
-
- {Array.from({ length: 3 }).map((_, subIndex) => ( -
- - -
- ))} -
-
- ))} -
- ) : ( - - )} -
-
- - - {selectedMailbox &&
- -
-

setOpen("mailbox")}> - {selectedMailbox?.name} -

-
- -
- - { - const dateA = a.date; - const dateB = b.date; - return dateB - dateA; - })} - /> - - {selectedMailbox &&
- -
} -
-
} - {!selectedMailbox &&
-
- Bichon Logo -
-
- } -
-
-
- setOpen('mailbox')} - /> - setOpen('display')} - /> - setOpen('move-to-trash')} - /> - - setOpen('restore')} - /> - setOpen('delete')} - /> - -
- ) -} \ No newline at end of file diff --git a/web/src/features/mailbox/components/mailbox-detail.tsx b/web/src/features/mailbox/components/mailbox-detail.tsx deleted file mode 100644 index 5645ad6..0000000 --- a/web/src/features/mailbox/components/mailbox-detail.tsx +++ /dev/null @@ -1,92 +0,0 @@ -// -// Copyright (c) 2025-2026 rustmailer.com (https://rustmailer.com) -// -// This file is part of the Bichon Email Archiving Project -// -// This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU Affero General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU Affero General Public License for more details. -// -// You should have received a copy of the GNU Affero General Public License -// along with this program. If not, see . - - -import { Button } from '@/components/ui/button' -import { - Dialog, - DialogClose, - DialogContent, - DialogFooter, - DialogHeader, - DialogTitle, -} from '@/components/ui/dialog' - -import AceEditor from '@/components/ace-editor' -import { useTheme } from '@/context/theme-context' -import { MailboxData } from '@/api/mailbox/api' -import { useMailboxContext } from '../context' -import { useTranslation } from 'react-i18next' - - -interface Props { - open: boolean - onOpenChange: (open: boolean) => void -} - -function convertMailboxData(raw: MailboxData): any { - - const attributes: string[] = []; - raw.attributes.forEach(item => { - attributes.push(item.attr); - if (item.attr.toLowerCase() === "Extension" && item.extension !== null) { - attributes.push(item.extension); - } - }); - - return { - // ...raw, - id: raw.id.toString(), - attributes - }; -} - - -export function MailboxDialog({ open, onOpenChange }: Props) { - const { theme } = useTheme() - const { currentMailbox } = useMailboxContext() - const { t } = useTranslation() - return ( - { - onOpenChange(state) - }} - > - - - {currentMailbox?.name} - - - - - - - - - - ) -} diff --git a/web/src/features/mailbox/components/restore-message-dialog.tsx b/web/src/features/mailbox/components/restore-message-dialog.tsx deleted file mode 100644 index e87c1ee..0000000 --- a/web/src/features/mailbox/components/restore-message-dialog.tsx +++ /dev/null @@ -1,106 +0,0 @@ -// -// Copyright (c) 2025-2026 rustmailer.com (https://rustmailer.com) -// -// This file is part of the Bichon Email Archiving Project -// -// This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU Affero General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU Affero General Public License for more details. -// -// You should have received a copy of the GNU Affero General Public License -// along with this program. If not, see . - - -import { restore_message } from '@/api/mailbox/envelope/api' -import { ConfirmDialog } from '@/components/confirm-dialog' -import { toast } from '@/hooks/use-toast' -import { useMutation } from '@tanstack/react-query' -import { AxiosError } from 'axios' -import { useTranslation } from 'react-i18next' -import { useMailboxContext } from '../context' -import { ToastAction } from '@/components/ui/toast' - -interface RestoreMessageDialogProps { - open: boolean - onOpenChange: (open: boolean) => void -} - -export function RestoreMessageDialog({ - open, - onOpenChange -}: RestoreMessageDialogProps) { - const { t } = useTranslation() - const { selectedAccountId, selected, setSelected } = useMailboxContext(); - - - const restoreMutation = useMutation({ - mutationFn: (messageIds: number[]) => - restore_message(selectedAccountId!, messageIds), - onSuccess: handleRestoreSuccess, - onError: handleRestoreError, - }); - - function handleRestoreSuccess() { - toast({ - title: t('restore_message.success', 'Messages restored'), - description: t( - 'restore_message.successDesc', - 'The selected messages have been restored to the IMAP server.' - ), - action: ( - - {t('common.close')} - - ), - }); - setSelected(new Set()); - onOpenChange(false); - } - - function handleRestoreError(error: AxiosError) { - const errorMessage = - (error.response?.data as { message?: string })?.message || - error.message || - t('restore_message.failed', 'Failed to restore messages'); - - toast({ - variant: 'destructive', - title: t( - 'restore_message.failedTitle', - 'Restore failed' - ), - description: errorMessage, - action: ( - - {t('common.tryAgain')} - - ), - }); - - console.error(error); - } - - - return ( - restoreMutation.mutate(Array.from(selected))} - className="sm:max-w-sm" - isLoading={restoreMutation.isPending} - disabled={restoreMutation.isPending} - /> - ) -} diff --git a/web/src/features/mailbox/components/thread-dialog.tsx b/web/src/features/mailbox/components/thread-dialog.tsx deleted file mode 100644 index 965ea90..0000000 --- a/web/src/features/mailbox/components/thread-dialog.tsx +++ /dev/null @@ -1,223 +0,0 @@ -// -// Copyright (c) 2025-2026 rustmailer.com (https://rustmailer.com) -// -// This file is part of the Bichon Email Archiving Project -// -// This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU Affero General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU Affero General Public License for more details. -// -// You should have received a copy of the GNU Affero General Public License -// along with this program. If not, see . - -import { useState } from 'react'; -import { useInfiniteQuery } from '@tanstack/react-query'; -import { format } from 'date-fns'; -import { ChevronDown, ChevronUp, Loader2, MessageSquareText } from 'lucide-react'; -import { useTranslation } from 'react-i18next'; - -import { - Dialog, - DialogContent, - DialogHeader, - DialogTitle, -} from '@/components/ui/dialog'; -import { Button } from '@/components/ui/button'; -import { Card, CardContent, CardHeader } from '@/components/ui/card'; -import { Skeleton } from '@/components/ui/skeleton'; -import { useMailboxContext } from '../context'; -import { get_thread_messages } from '@/api/mailbox/envelope/api'; -import { MailMessageView } from './mail-message-view'; - -interface MailThreadDialogProps { - open: boolean; - onOpenChange: (open: boolean) => void; -} - -export function MailThreadDialog({ open, onOpenChange }: MailThreadDialogProps) { - const { t } = useTranslation(); - const { selectedAccountId, currentEnvelope } = useMailboxContext(); - const [expandedIds, setExpandedIds] = useState>(new Set()); - - const threadId = currentEnvelope?.thread_id; - const accountId = selectedAccountId; - - const { - data, - fetchNextPage, - hasNextPage, - isFetchingNextPage, - isLoading, - isError, - error, - } = useInfiniteQuery({ - queryKey: ['thread', accountId, threadId], - queryFn: ({ pageParam = 1 }) => - get_thread_messages(accountId!, threadId!, pageParam, 10), - getNextPageParam: (lastPage) => - lastPage.current_page && lastPage.total_pages - ? lastPage.current_page < lastPage.total_pages - ? lastPage.current_page + 1 - : undefined - : undefined, - enabled: open && !!accountId && !!threadId, - initialPageParam: 1, - }); - - const allMessages = data?.pages.flatMap((page) => page.items) ?? []; - const totalCount = data?.pages[0]?.total_items ?? 0; - - const toggleExpand = (id: number) => { - setExpandedIds((prev) => { - const next = new Set(prev); - next.has(id) ? next.delete(id) : next.add(id); - return next; - }); - }; - - return ( - - - {/* Header */} - -
- - -
- {t('mailbox.thread.title', { - count: totalCount, - messageLabel: totalCount === 1 ? t('mailbox.thread.message') : t('mailbox.thread.messages') - })} -
-
-
-
- - {/* Body */} -
- {isLoading && } - - {isError && ( -
- {t('mailbox.thread.loadError')}: {(error as Error)?.message} -
- )} - - {!isLoading && allMessages.length === 0 && ( -
- {t('mailbox.thread.empty')} -
- )} - - {allMessages - .sort((a, b) => a.date - b.date) - .map((msg) => { - const isExpanded = expandedIds.has(msg.id); - const preview = msg.text?.slice(0, 120) + (msg.text?.length > 120 ? '...' : ''); - const date = new Date(msg.date); - const formattedDate = isNaN(date.getTime()) - ? t('mailbox.thread.invalidDate') - : format(date, 'yyyy-MM-dd HH:mm:ss'); - - return ( - - toggleExpand(msg.id)} - > -
-
-
- {msg.from} - - - {msg.to.join(', ')} - -
-

- {msg.subject || t('mailbox.thread.noSubject')} -

- {!isExpanded && preview && ( -

- {preview} -

- )} -
-
- {formattedDate} - {isExpanded ? ( - - ) : ( - - )} -
-
-
- - {isExpanded && ( - -
- -
-
- )} -
- ); - })} - - {hasNextPage && ( -
- -
- )} -
-
-
- ); -} - -// Skeleton -function ThreadSkeleton() { - return ( -
- {[...Array(3)].map((_, i) => ( - - - - - - - - - ))} -
- ); -} diff --git a/web/src/features/mailbox/context/index.tsx b/web/src/features/mailbox/context/index.tsx deleted file mode 100644 index 038a839..0000000 --- a/web/src/features/mailbox/context/index.tsx +++ /dev/null @@ -1,63 +0,0 @@ -// -// Copyright (c) 2025-2026 rustmailer.com (https://rustmailer.com) -// -// This file is part of the Bichon Email Archiving Project -// -// This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU Affero General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU Affero General Public License for more details. -// -// You should have received a copy of the GNU Affero General Public License -// along with this program. If not, see . - - -import React from 'react' -import { MailboxData } from '@/api/mailbox/api' -import { EmailEnvelope } from '@/api' - -export type MailboxDialogType = 'mailbox' | 'display' | 'move-to-trash' | 'filters' | 'restore' | 'delete' - -interface MailboxContextType { - open: MailboxDialogType | null - setOpen: (str: MailboxDialogType | null) => void - selectedAccountId: number | undefined - currentMailbox: MailboxData | undefined - currentEnvelope: EmailEnvelope | undefined - setCurrentMailbox: React.Dispatch> - deleteMailboxId: string | undefined, - setDeleteMailboxId: React.Dispatch> - setCurrentEnvelope: React.Dispatch> - deleteIds: Set - setDeleteIds: React.Dispatch>> - selected: Set - setSelected: React.Dispatch>> -} - -const MailboxContext = React.createContext(null) - -interface Props { - children: React.ReactNode - value: MailboxContextType -} - -export default function MailboxProvider({ children, value }: Props) { - return {children} -} - -export const useMailboxContext = () => { - const mailboxContext = React.useContext(MailboxContext) - - if (!mailboxContext) { - throw new Error( - 'useMailboxContext has to be used within ' - ) - } - - return mailboxContext -} diff --git a/web/src/features/mailbox/index.tsx b/web/src/features/mailbox/index.tsx deleted file mode 100644 index b1ba8df..0000000 --- a/web/src/features/mailbox/index.tsx +++ /dev/null @@ -1,46 +0,0 @@ -// -// Copyright (c) 2025-2026 rustmailer.com (https://rustmailer.com) -// -// This file is part of the Bichon Email Archiving Project -// -// This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU Affero General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU Affero General Public License for more details. -// -// You should have received a copy of the GNU Affero General Public License -// along with this program. If not, see . - - -import { Mail } from "./components/mail" -import { Main } from "@/components/layout/main" -import { FixedHeader } from "@/components/layout/fixed-header" - -export default function Mailboxes() { - const layout = localStorage.getItem("react-resizable-panels:layout:mail") - const collapsed = localStorage.getItem("react-resizable-panels:collapsed") - - const defaultLayout = layout ? JSON.parse(layout) : undefined - const defaultCollapsed = collapsed ? JSON.parse(collapsed) : undefined - - const lastSelectedAccountId = localStorage.getItem('mailbox:selectedAccountId') ?? undefined - - return ( - <> - -
- -
- - ) -} \ No newline at end of file diff --git a/web/src/features/search/account-mailbox-filter.tsx b/web/src/features/search/account-mailbox-filter.tsx deleted file mode 100644 index 85831ce..0000000 --- a/web/src/features/search/account-mailbox-filter.tsx +++ /dev/null @@ -1,11 +0,0 @@ -import { AccountPopover } from './account-popover' -import { MailboxPopover } from './mailbox-popover' - -export function AccountMailboxFilter() { - return ( - <> - - - - ) -} diff --git a/web/src/features/search/bulk-actions.tsx b/web/src/features/search/bulk-actions.tsx index a43009b..0956dfe 100644 --- a/web/src/features/search/bulk-actions.tsx +++ b/web/src/features/search/bulk-actions.tsx @@ -138,7 +138,6 @@ export function MailBulkActions({ children }: MailBulkActionsProps) { 'flex items-center gap-x-2' )} > - {/* Clear Selection */} - {t('search.bulkActions.restoreDesc')} + {t('restore_message.restore_to_imap', 'Restore Mail')} - {/* Delete */} diff --git a/web/src/features/search/context/index.tsx b/web/src/features/search/context/index.tsx index 19005cd..70050cf 100644 --- a/web/src/features/search/context/index.tsx +++ b/web/src/features/search/context/index.tsx @@ -21,7 +21,7 @@ import React from 'react' import { EmailEnvelope } from '@/api' import { SortingState } from '@tanstack/react-table' -export type SearchDialogType = 'mailbox' | 'display' | 'delete' | 'filters' | 'tags' | 'edit-tags' | 'restore' +export type SearchDialogType = 'mailbox' | 'display' | 'delete' | 'filters' | 'tags' | 'edit-tags' | 'restore' | 'delete-mailbox' interface SearchContextType { open: SearchDialogType | null @@ -32,6 +32,10 @@ interface SearchContextType { setToDelete: React.Dispatch>>> selected: Map> setSelected: React.Dispatch>>> + deleteMailboxId: string | undefined + setDeleteMailboxId: React.Dispatch> + selectedAccountId: number | undefined + setSelectedAccountId: React.Dispatch> selectedTags: string[] sorting: SortingState setSorting: React.Dispatch> diff --git a/web/src/features/mailbox/components/delete-mailbox-dialog.tsx b/web/src/features/search/delete-mailbox-dialog.tsx similarity index 95% rename from web/src/features/mailbox/components/delete-mailbox-dialog.tsx rename to web/src/features/search/delete-mailbox-dialog.tsx index 6afaca3..dafb2db 100644 --- a/web/src/features/mailbox/components/delete-mailbox-dialog.tsx +++ b/web/src/features/search/delete-mailbox-dialog.tsx @@ -21,9 +21,9 @@ import { toast } from '@/hooks/use-toast'; import { Alert, AlertDescription, AlertTitle } from '@/components/ui/alert'; import { ConfirmDialog } from '@/components/confirm-dialog'; import { useMutation, useQueryClient } from '@tanstack/react-query'; -import { useMailboxContext } from '../context'; import { useTranslation } from 'react-i18next'; import { delete_mailbox } from '@/api/mailbox/api'; +import { useSearchContext } from './context'; interface Props { open: boolean; @@ -32,7 +32,7 @@ interface Props { export function MailBoxDeleteDialog({ open, onOpenChange }: Props) { const queryClient = useQueryClient(); - const { selectedAccountId, deleteMailboxId, setDeleteMailboxId } = useMailboxContext(); + const { selectedAccountId, deleteMailboxId, setDeleteMailboxId } = useSearchContext(); const { t } = useTranslation(); const deleteMutation = useMutation({ @@ -40,7 +40,7 @@ export function MailBoxDeleteDialog({ open, onOpenChange }: Props) { delete_mailbox(accountId, mailboxId), retry: false, onSuccess: () => { - queryClient.invalidateQueries({ queryKey: ['account-mailboxes', `${selectedAccountId}`] }); + queryClient.invalidateQueries({ queryKey: ['search-mailboxes', selectedAccountId] }); onOpenChange(false); setDeleteMailboxId(undefined); toast({ diff --git a/web/src/features/search/index.tsx b/web/src/features/search/index.tsx index e9c1d50..ec98c1f 100644 --- a/web/src/features/search/index.tsx +++ b/web/src/features/search/index.tsx @@ -33,6 +33,7 @@ import { useTranslation } from 'react-i18next'; import { RestoreMessageDialog } from './restore-message-dialog'; import { MailListTable } from './mail-list-table'; import { SortingState } from '@tanstack/react-table'; +import { MailBoxDeleteDialog } from './delete-mailbox-dialog'; export default function Search() { const { t } = useTranslation() @@ -42,6 +43,8 @@ export default function Search() { const [selected, setSelected] = React.useState>>(new Map()); const [selectedTags, setSelectedTags] = React.useState([]); const [sorting, setSorting] = React.useState([{ id: "date", desc: true }]); + const [deleteMailboxId, setDeleteMailboxId] = React.useState(undefined); + const [selectedAccountId, setSelectedAccountId] = React.useState(undefined); const { emails, @@ -90,6 +93,10 @@ export default function Search() { setSorting, filter, setFilter, + deleteMailboxId, + setDeleteMailboxId, + selectedAccountId, + setSelectedAccountId, handleTagToggle }} > @@ -152,6 +159,12 @@ export default function Search() { open={open === 'restore'} onOpenChange={() => setOpen('restore')} /> + + setOpen('delete-mailbox')} + /> diff --git a/web/src/features/search/mail-list.tsx b/web/src/features/search/mail-list.tsx index 06c76b0..67e7b90 100644 --- a/web/src/features/search/mail-list.tsx +++ b/web/src/features/search/mail-list.tsx @@ -252,7 +252,7 @@ export function MailList({ }} > - {t('restore_message.restore_to_imap', 'Restore Mail')} + {t('restore_message.restore_to_imap')} ; +} + +interface CustomTreeItemProps + extends Omit, + Omit, 'onFocus'> { } + +interface CustomLabelProps { + exists?: number; + attributes?: { attr: string; extension: string | null }[], + children: React.ReactNode; + id: string; + icon?: React.ElementType; + expandable?: boolean; + onDelete: (id: string) => void; +} + +function CustomLabel({ + expandable, + exists, + attributes, + children, + id, + onDelete, + ...other +}: CustomLabelProps) { + const { t } = useTranslation() + return ( + + + + {children} + +
+ + + + + + { + e.stopPropagation(); + }} + onSelect={(e) => { + e.preventDefault(); + onDelete(id); + }} + > + + {t('common.delete')} + + + +
+
+ ); +} export function MailboxPopover() { - const { t } = useTranslation() - const { filter, setFilter } = useSearchContext() - const { minimalList = [] } = useMinimalAccountList() + const { t } = useTranslation(); + const { filter, setFilter, setOpen, setDeleteMailboxId, setSelectedAccountId } = useSearchContext(); + const { minimalList = [] } = useMinimalAccountList(); - const [search, setSearch] = React.useState('') + const [localOpen, setLocalOpen] = React.useState(false); + const [search, setSearch] = React.useState(''); - const accountIds: number[] = filter.account_ids ?? [] - const selectedMailboxIds: number[] = filter.mailbox_ids ?? [] + const accountIds: number[] = filter.account_ids ?? []; + const selectedMailboxIds: number[] = filter.mailbox_ids ?? []; - const { mailboxes, isLoading } = useQueries({ - queries: accountIds.map(id => ({ - queryKey: ['search-mailboxes', id], - queryFn: () => list_mailboxes(id, false), - enabled: accountIds.length > 0, - })), - combine: results => ({ - mailboxes: results.flatMap(r => r.data ?? []), - isLoading: results.some(r => r.isLoading), - }), - }) + const [localSelectedIds, setLocalSelectedIds] = React.useState([]); + const [activeAccountId, setActiveAccountId] = React.useState(undefined); - const toggleMailbox = (id: number) => { - setFilter(prev => { - const next = { ...prev } - const set = new Set(next.mailbox_ids ?? []) + const queryClient = useQueryClient(); - set.has(id) ? set.delete(id) : set.add(id) + React.useEffect(() => { + if (localOpen) { + const globalMailboxIds = filter.mailbox_ids ?? []; + setLocalSelectedIds(globalMailboxIds); - const ids = Array.from(set) - - if (ids.length === 0) delete next.mailbox_ids - else next.mailbox_ids = ids - - return next - }) - } - - const clearAllMailboxes = () => { - setFilter(prev => { - const next = { ...prev } - delete next.mailbox_ids - return next - }) - } - - const grouped = React.useMemo(() => { - const q = search.trim().toLowerCase() - const map = new Map() - - for (const mb of mailboxes) { - if (q && !mb.name.toLowerCase().includes(q)) continue - if (!map.has(mb.account_id)) map.set(mb.account_id, []) - map.get(mb.account_id)!.push(mb) + const currentAccountIds = filter.account_ids ?? []; + if (currentAccountIds.length > 0) { + if (!activeAccountId || !currentAccountIds.includes(activeAccountId)) { + setActiveAccountId(currentAccountIds[0]); + } + } else { + setActiveAccountId(undefined); + } } + }, [localOpen, activeAccountId, filter.account_ids, filter.mailbox_ids]); - for (const list of map.values()) { - list.sort((a, b) => { - const aSel = selectedMailboxIds.includes(a.id) - const bSel = selectedMailboxIds.includes(b.id) - if (aSel && !bSel) return -1 - if (!aSel && bSel) return 1 - return a.name.localeCompare(b.name) - }) - } + const { data: activeMailboxes = [], isLoading: activeIsLoading } = useQuery({ + queryKey: ['search-mailboxes', activeAccountId], + queryFn: () => list_mailboxes(activeAccountId!, false), + enabled: !!activeAccountId, + }); - return Array.from(map.entries()) - }, [mailboxes, search, selectedMailboxIds]) + const treeData = React.useMemo(() => { + const filtered = search.trim() + ? activeMailboxes.filter(m => m.name.toLowerCase().includes(search.toLowerCase())) + : activeMailboxes; + return buildTree(filtered); + }, [activeMailboxes, search]); - const defaultOpen = grouped - .filter(([, boxes]) => - boxes.some(m => selectedMailboxIds.includes(m.id)) - ) - .map(([id]) => id.toString()) + const disabled = accountIds.length === 0; - const getAccountEmail = (id: number) => - minimalList.find(a => a.id === id)?.email ?? '' + const handleApply = () => { + setFilter(prev => ({ + ...prev, + mailbox_ids: localSelectedIds.length > 0 ? localSelectedIds : undefined + })); + setLocalOpen(false); + }; - const disabled = accountIds.length === 0 + const handleDeleteClick = (id: string) => { + console.log("delete=", id); + setDeleteMailboxId(id); + setSelectedAccountId(activeAccountId); + setOpen('delete-mailbox'); + }; + + + const CustomTreeItem = React.forwardRef(function CustomTreeItem( + props: CustomTreeItemProps, + ref: React.Ref, + ) { + const { id, itemId, label, disabled, children, ...other } = props; + const { + getContextProviderProps, + getRootProps, + getContentProps, + getLabelProps, + getIconContainerProps, + getCheckboxProps, + getGroupTransitionProps, + getDragAndDropOverlayProps, + status, + } = useTreeItem({ id, itemId, children, label, disabled, rootRef: ref }); + + const item = useTreeItemModel(itemId)!; + + + return ( + + + + + + + + + + + + {children && } + + + ); + }); return ( - + - -
- setSearch(e.target.value)} - placeholder={t('search_mailbox.search_placeholder')} - className="h-8 text-sm" - /> -
- {selectedMailboxIds.length > 0 && ( -
+ +
+
+ + setSearch(e.target.value)} + placeholder={t('search_mailbox.search_placeholder')} + className="h-9 pl-8 text-xs bg-background" + /> +
+ {localSelectedIds.length > 0 && ( -
- )} - - {disabled ? ( -

- {t('search_mailbox.select_account_first')} -

- ) : isLoading ? ( -
- {Array.from({ length: 6 }).map((_, i) => ( -
- ))} -
- ) : grouped.length === 0 ? ( -

- {t('search_mailbox.no_mailbox_found')} -

- ) : ( - - {grouped.map(([accountId, boxes]) => { - const selectedCount = boxes.filter(b => - selectedMailboxIds.includes(b.id) - ).length + )} +
- return ( - - - - {getAccountEmail(accountId)} +
+
+ +
+ {accountIds.map(id => { + const acc = minimalList.find(a => a.id === id); + const isActive = activeAccountId === id; + const cachedData = queryClient.getQueryData(['search-mailboxes', id]); + const count = cachedData?.filter(m => localSelectedIds.includes(m.id)).length ?? 0; + + return ( + + ); + })} +
+
+
+
+ +
+ {activeIsLoading ? ( +
+ {[1, 2, 3, 4, 5].map(i => ( +
+ ))} +
+ ) : activeAccountId ? ( + { + setLocalSelectedIds(itemIds.map(id => parseInt(id)).filter(id => !isNaN(id))); + }} + slots={{ item: CustomTreeItem }} + sx={{ width: '100%' }} + /> + ) : ( +
+ +

{t('search_mailbox.select_account_tip')}

+
+ )} +
+ - -
- {boxes.map(mailbox => { - const checked = - selectedMailboxIds.includes(mailbox.id) - - return ( - - - -
- toggleMailbox(mailbox.id) - } - className={cn( - 'flex items-center gap-2 px-2 py-1.5 rounded-md cursor-pointer', - 'hover:bg-accent transition-colors', - checked && - 'bg-primary/10 text-primary' - )} - > - - toggleMailbox(mailbox.id) - } - onClick={e => - e.stopPropagation() - } - /> - - - {mailbox.name} - -
-
- - -
- {mailbox.name} -
-
-
-
- ) - })} -
-
- - ) - })} - - )} - +
+
+ {t('search_mailbox.selected_total')}: {localSelectedIds.length} +
+
+ + +
+
+
+
- - ) + + ); } \ No newline at end of file diff --git a/web/src/features/search/table/toolbar.tsx b/web/src/features/search/table/toolbar.tsx index a3f3cf6..852ceaa 100644 --- a/web/src/features/search/table/toolbar.tsx +++ b/web/src/features/search/table/toolbar.tsx @@ -1,12 +1,13 @@ import { type Table } from '@tanstack/react-table' import { DataTableViewOptions } from './view-options' import { TagFilterPopover } from '../tag-filter-popover' -import { AccountMailboxFilter } from '../account-mailbox-filter' import { TimePopover } from '../time-popover' import { MailFilterPopover } from '../contact-popover' import { TextSearchInput } from '../text-search-input' import { MoreFiltersPopover } from '../more-filters-popover' import { FilterResetButton } from '../filter-reset' +import { MailboxPopover } from '../mailbox-popover' +import { AccountPopover } from '../account-popover' type DataTableToolbarProps = { table: Table @@ -25,9 +26,10 @@ export function DataTableToolbar({
- - + + + diff --git a/web/src/features/settings/api-tokens/token-list.tsx b/web/src/features/settings/api-tokens/token-list.tsx index e644fba..c9d3dcc 100644 --- a/web/src/features/settings/api-tokens/token-list.tsx +++ b/web/src/features/settings/api-tokens/token-list.tsx @@ -144,8 +144,6 @@ export const TokenCardList: React.FC = ({ tokens, userId }) => {
- - {/* Meta */}
diff --git a/web/src/routeTree.gen.ts b/web/src/routeTree.gen.ts index 813f51b..6a76ca0 100644 --- a/web/src/routeTree.gen.ts +++ b/web/src/routeTree.gen.ts @@ -46,9 +46,6 @@ const AuthenticatedOauth2IndexLazyImport = createFileRoute( const AuthenticatedOauth2ResultIndexLazyImport = createFileRoute( '/_authenticated/oauth2-result/', )() -const AuthenticatedMailboxesIndexLazyImport = createFileRoute( - '/_authenticated/mailboxes/', -)() const AuthenticatedApiDocsIndexLazyImport = createFileRoute( '/_authenticated/api-docs/', )() @@ -207,15 +204,6 @@ const AuthenticatedOauth2ResultIndexLazyRoute = ), ) -const AuthenticatedMailboxesIndexLazyRoute = - AuthenticatedMailboxesIndexLazyImport.update({ - id: '/mailboxes/', - path: '/mailboxes/', - getParentRoute: () => AuthenticatedRouteRoute, - } as any).lazy(() => - import('./routes/_authenticated/mailboxes/index.lazy').then((d) => d.Route), - ) - const AuthenticatedApiDocsIndexLazyRoute = AuthenticatedApiDocsIndexLazyImport.update({ id: '/api-docs/', @@ -451,13 +439,6 @@ declare module '@tanstack/react-router' { preLoaderRoute: typeof AuthenticatedApiDocsIndexLazyImport parentRoute: typeof AuthenticatedRouteImport } - '/_authenticated/mailboxes/': { - id: '/_authenticated/mailboxes/' - path: '/mailboxes' - fullPath: '/mailboxes' - preLoaderRoute: typeof AuthenticatedMailboxesIndexLazyImport - parentRoute: typeof AuthenticatedRouteImport - } '/_authenticated/oauth2-result/': { id: '/_authenticated/oauth2-result/' path: '/oauth2-result' @@ -550,7 +531,6 @@ interface AuthenticatedRouteRouteChildren { AuthenticatedIndexRoute: typeof AuthenticatedIndexRoute AuthenticatedAccountsIndexLazyRoute: typeof AuthenticatedAccountsIndexLazyRoute AuthenticatedApiDocsIndexLazyRoute: typeof AuthenticatedApiDocsIndexLazyRoute - AuthenticatedMailboxesIndexLazyRoute: typeof AuthenticatedMailboxesIndexLazyRoute AuthenticatedOauth2ResultIndexLazyRoute: typeof AuthenticatedOauth2ResultIndexLazyRoute AuthenticatedOauth2IndexLazyRoute: typeof AuthenticatedOauth2IndexLazyRoute AuthenticatedSearchIndexLazyRoute: typeof AuthenticatedSearchIndexLazyRoute @@ -564,7 +544,6 @@ const AuthenticatedRouteRouteChildren: AuthenticatedRouteRouteChildren = { AuthenticatedIndexRoute: AuthenticatedIndexRoute, AuthenticatedAccountsIndexLazyRoute: AuthenticatedAccountsIndexLazyRoute, AuthenticatedApiDocsIndexLazyRoute: AuthenticatedApiDocsIndexLazyRoute, - AuthenticatedMailboxesIndexLazyRoute: AuthenticatedMailboxesIndexLazyRoute, AuthenticatedOauth2ResultIndexLazyRoute: AuthenticatedOauth2ResultIndexLazyRoute, AuthenticatedOauth2IndexLazyRoute: AuthenticatedOauth2IndexLazyRoute, @@ -594,7 +573,6 @@ export interface FileRoutesByFullPath { '/users/roles': typeof AuthenticatedUsersRolesLazyRoute '/accounts': typeof AuthenticatedAccountsIndexLazyRoute '/api-docs': typeof AuthenticatedApiDocsIndexLazyRoute - '/mailboxes': typeof AuthenticatedMailboxesIndexLazyRoute '/oauth2-result': typeof AuthenticatedOauth2ResultIndexLazyRoute '/oauth2': typeof AuthenticatedOauth2IndexLazyRoute '/search': typeof AuthenticatedSearchIndexLazyRoute @@ -619,7 +597,6 @@ export interface FileRoutesByTo { '/users/roles': typeof AuthenticatedUsersRolesLazyRoute '/accounts': typeof AuthenticatedAccountsIndexLazyRoute '/api-docs': typeof AuthenticatedApiDocsIndexLazyRoute - '/mailboxes': typeof AuthenticatedMailboxesIndexLazyRoute '/oauth2-result': typeof AuthenticatedOauth2ResultIndexLazyRoute '/oauth2': typeof AuthenticatedOauth2IndexLazyRoute '/search': typeof AuthenticatedSearchIndexLazyRoute @@ -649,7 +626,6 @@ export interface FileRoutesById { '/_authenticated/users/roles': typeof AuthenticatedUsersRolesLazyRoute '/_authenticated/accounts/': typeof AuthenticatedAccountsIndexLazyRoute '/_authenticated/api-docs/': typeof AuthenticatedApiDocsIndexLazyRoute - '/_authenticated/mailboxes/': typeof AuthenticatedMailboxesIndexLazyRoute '/_authenticated/oauth2-result/': typeof AuthenticatedOauth2ResultIndexLazyRoute '/_authenticated/oauth2/': typeof AuthenticatedOauth2IndexLazyRoute '/_authenticated/search/': typeof AuthenticatedSearchIndexLazyRoute @@ -679,7 +655,6 @@ export interface FileRouteTypes { | '/users/roles' | '/accounts' | '/api-docs' - | '/mailboxes' | '/oauth2-result' | '/oauth2' | '/search' @@ -703,7 +678,6 @@ export interface FileRouteTypes { | '/users/roles' | '/accounts' | '/api-docs' - | '/mailboxes' | '/oauth2-result' | '/oauth2' | '/search' @@ -731,7 +705,6 @@ export interface FileRouteTypes { | '/_authenticated/users/roles' | '/_authenticated/accounts/' | '/_authenticated/api-docs/' - | '/_authenticated/mailboxes/' | '/_authenticated/oauth2-result/' | '/_authenticated/oauth2/' | '/_authenticated/search/' @@ -790,7 +763,6 @@ export const routeTree = rootRoute "/_authenticated/", "/_authenticated/accounts/", "/_authenticated/api-docs/", - "/_authenticated/mailboxes/", "/_authenticated/oauth2-result/", "/_authenticated/oauth2/", "/_authenticated/search/" @@ -878,10 +850,6 @@ export const routeTree = rootRoute "filePath": "_authenticated/api-docs/index.lazy.tsx", "parent": "/_authenticated" }, - "/_authenticated/mailboxes/": { - "filePath": "_authenticated/mailboxes/index.lazy.tsx", - "parent": "/_authenticated" - }, "/_authenticated/oauth2-result/": { "filePath": "_authenticated/oauth2-result/index.lazy.tsx", "parent": "/_authenticated" diff --git a/web/src/routes/_authenticated/mailboxes/index.lazy.tsx b/web/src/routes/_authenticated/mailboxes/index.lazy.tsx deleted file mode 100644 index 4be3d28..0000000 --- a/web/src/routes/_authenticated/mailboxes/index.lazy.tsx +++ /dev/null @@ -1,25 +0,0 @@ -// -// Copyright (c) 2025-2026 rustmailer.com (https://rustmailer.com) -// -// This file is part of the Bichon Email Archiving Project -// -// This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU Affero General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU Affero General Public License for more details. -// -// You should have received a copy of the GNU Affero General Public License -// along with this program. If not, see . - - -import { createLazyFileRoute } from '@tanstack/react-router' -import Mailboxes from '@/features/mailbox' - -export const Route = createLazyFileRoute('/_authenticated/mailboxes/')({ - component: Mailboxes, -})