feat: nested eml quick view

This commit is contained in:
rustmailer
2026-04-22 09:40:52 +08:00
parent c3a12eafb2
commit 5b884125f7
25 changed files with 109 additions and 37 deletions
+2 -2
View File
@@ -41,10 +41,10 @@ export const download_attachment = async (accountId: number, id: string, content
saveAs(blob, fileName);
};
export const download_nested_attachment = async (accountId: number, id: string, content_hash: string, nested_content_hash: string) => {
export const download_nested_attachment = async (accountId: number, id: string, content_hash: string, nested_content_hash: string, fileName: string) => {
const response = await axiosInstance.get(`api/v1/download-nested-attachment/${accountId}/${id}?content_hash=${content_hash}&nested_content_hash=${nested_content_hash}`, { responseType: 'blob' });
const blob = new Blob([response.data]);
saveAs(blob, nested_content_hash);
saveAs(blob, fileName);
};
export interface AttachmentInfo {
/** MIME content type of the attachment (e.g., `image/png`, `application/pdf`). */
@@ -21,7 +21,7 @@ import React from 'react'
import { SortingState } from '@tanstack/react-table'
import { AttachmentModel } from '@/api/attachment/api'
export type AttachmentDialogType = 'mailbox' | 'display' | 'delete' | 'filters' | 'tags' | 'edit-tags' | 'update-tags' | 'restore' | 'delete-mailbox'
export type AttachmentDialogType = 'mailbox' | 'display' | 'delete' | 'filters' | 'tags' | 'edit-tags' | 'update-tags' | 'restore' | 'delete-mailbox' | 'nested-eml'
interface AttachmentContextType {
open: AttachmentDialogType | null
+7
View File
@@ -32,6 +32,7 @@ import { AttachmentModel } from '@/api/attachment/api';
import { MailDisplayDrawer } from './mail-display-dialog';
import { EnvelopeDeleteDialog } from './delete-dialog';
import { RestoreMessageDialog } from './restore-message-dialog';
import { NestedEmailDialog } from './nested-email-dialog';
export default function AttachmentSearch() {
const { t } = useTranslation()
@@ -147,6 +148,12 @@ export default function AttachmentSearch() {
open={open === 'restore'}
onOpenChange={() => setOpen('restore')}
/>
<NestedEmailDialog
key="nested-eml-attachment-dialog"
open={open === 'nested-eml'}
onOpenChange={() => setOpen('nested-eml')}
/>
</AttachmentProvider>
</Main>
</>
@@ -165,7 +165,7 @@ export function AttachmentListTable({
accessorKey: "name",
header: t('attachment.name'),
cell: ({ row }) => {
const { name, content_type } = row.original;
const { name, content_type, is_message } = row.original;
const safeName = name ?? "n/a";
const shortContentType = content_type
@@ -179,9 +179,30 @@ export function AttachmentListTable({
contentType={content_type ?? ""}
className="h-4 w-4 mt-0.5"
/>
<LongText className='text-xs font-medium max-w-[320px] text-foreground/90'>
{is_message && <div className="group relative flex items-center w-full min-w-0 h-full px-2 overflow-hidden">
<div className="absolute left-0 top-0 bottom-0 w-[2px] bg-primary opacity-0 group-hover:opacity-100 transition-opacity" />
<div className="text-xs flex flex-wrap gap-x-1 min-w-0 flex-1">
<span className="flex items-center">
<button
type="button"
title={t('attachment.viewEmbeddedEmail')}
onClick={(e) => {
e.stopPropagation();
setCurrentAttachment(row.original);
setOpen("nested-eml");
}}
className="hover:text-primary hover:underline transition-colors truncate"
>
<LongText>{safeName}</LongText>
</button>
</span>
</div>
</div>}
{!is_message && <LongText className='text-xs font-medium max-w-[320px] text-foreground/90'>
{safeName}
</LongText>
</LongText>}
</div>
<div className="flex items-center gap-1 ml-6.5 mt-1">
<span className="text-[10px] text-muted-foreground font-mono bg-muted px-1 py-0.5 rounded-sm">
@@ -278,7 +299,7 @@ export function AttachmentListTable({
if (isLoading) {
return (
<div className="divide-y divide-border">
{Array.from({ length: 8 }).map((_, i) => (
{Array.from({ length: 30 }).map((_, i) => (
<div key={i} className="flex items-center gap-2 px-2 py-1.5">
<Skeleton className="h-3 w-3" />
<Skeleton className="h-3 w-3 rounded-full" />
@@ -27,6 +27,7 @@ import { useQuery } from '@tanstack/react-query';
import { Download, Loader, Mail } from 'lucide-react';
import { useTranslation } from 'react-i18next';
import { getFileConfig } from './mail-message-view';
import { useAttachmentContext } from './context';
const MessageHeader = ({
envelope,
@@ -35,7 +36,7 @@ const MessageHeader = ({
}: {
envelope: EmailEnvelope,
attachments?: AttachmentInfo[],
onDownload: (nested_content_hash: string) => void
onDownload: (nested_content_hash: string, fileName: string) => void
}) => {
const { t } = useTranslation();
const displayAttachments = attachments || [];
@@ -118,7 +119,7 @@ const MessageHeader = ({
<Tooltip key={i}>
<TooltipTrigger asChild>
<button
onClick={() => onDownload(att.content_hash)}
onClick={() => onDownload(att.content_hash, att.filename)}
className="group flex items-center gap-2 px-3 py-1.5 bg-slate-50 border border-slate-200 rounded-lg hover:bg-blue-50 hover:border-blue-200 transition-all text-slate-600 hover:text-blue-700"
>
<span className={`${color} p-0.5 rounded`}>{icon}</span>
@@ -144,12 +145,13 @@ const MessageHeader = ({
export function NestedEmailDialog({ open, onOpenChange, accountId, envelopeId, fileName, content_hash }: any) {
export function NestedEmailDialog({ open, onOpenChange }: any) {
const { currentAttachment } = useAttachmentContext()
const { data, isLoading } = useQuery({
queryKey: ['nested-message', accountId, envelopeId, content_hash],
queryFn: () => load_nested_message(accountId, envelopeId, content_hash),
enabled: open && !!content_hash,
queryKey: ['nested-message', currentAttachment?.account_id!, currentAttachment?.envelope_id!, currentAttachment?.content_hash!],
queryFn: () => load_nested_message(currentAttachment?.account_id!, currentAttachment?.envelope_id!, currentAttachment?.content_hash!),
enabled: open && !!currentAttachment,
});
return (
@@ -158,7 +160,7 @@ export function NestedEmailDialog({ open, onOpenChange, accountId, envelopeId, f
<div className="text-white px-4 py-3 flex items-center justify-between">
<div className="flex items-center gap-2">
<Mail className="h-4 w-4 text-blue-400" />
<span className="text-sm font-medium truncate max-w-[400px] opacity-90">{fileName}</span>
<span className="text-sm font-medium truncate max-w-[400px] opacity-90">{currentAttachment?.name}</span>
</div>
</div>
@@ -170,7 +172,13 @@ export function NestedEmailDialog({ open, onOpenChange, accountId, envelopeId, f
<MessageHeader
envelope={data.envelope}
attachments={data.attachments}
onDownload={(nested_content_hash) => download_nested_attachment(accountId, envelopeId, content_hash, nested_content_hash)}
onDownload={(nested_content_hash, fileName) => download_nested_attachment(
currentAttachment?.account_id!,
currentAttachment?.envelope_id!,
currentAttachment?.content_hash!,
nested_content_hash,
fileName
)}
/>
<div className="mt-8 pt-8 border-t border-slate-100">
+1 -1
View File
@@ -325,7 +325,7 @@ export function MailListTable({
if (isLoading) {
return (
<div className="divide-y divide-border">
{Array.from({ length: 8 }).map((_, i) => (
{Array.from({ length: 30 }).map((_, i) => (
<div key={i} className="flex items-center gap-2 px-2 py-1.5">
<Skeleton className="h-3 w-3" />
<Skeleton className="h-3 w-3 rounded-full" />
@@ -35,7 +35,7 @@ const MessageHeader = ({
}: {
envelope: EmailEnvelope,
attachments?: AttachmentInfo[],
onDownload: (nested_content_hash: string) => void
onDownload: (nested_content_hash: string, fileName: string) => void
}) => {
const { t } = useTranslation();
const displayAttachments = attachments || [];
@@ -118,7 +118,7 @@ const MessageHeader = ({
<Tooltip key={i}>
<TooltipTrigger asChild>
<button
onClick={() => onDownload(att.content_hash)}
onClick={() => onDownload(att.content_hash, att.filename)}
className="group flex items-center gap-2 px-3 py-1.5 bg-slate-50 border border-slate-200 rounded-lg hover:bg-blue-50 hover:border-blue-200 transition-all text-slate-600 hover:text-blue-700"
>
<span className={`${color} p-0.5 rounded`}>{icon}</span>
@@ -170,7 +170,7 @@ export function NestedEmailDialog({ open, onOpenChange, accountId, envelopeId, f
<MessageHeader
envelope={data.envelope}
attachments={data.attachments}
onDownload={(nested_content_hash) => download_nested_attachment(accountId, envelopeId, content_hash, nested_content_hash)}
onDownload={(nested_content_hash, fileName) => download_nested_attachment(accountId, envelopeId, content_hash, nested_content_hash, fileName)}
/>
<div className="mt-8 pt-8 border-t border-slate-100">
+3 -1
View File
@@ -375,7 +375,8 @@
"showDuplicates": "إظهار الملفات المكررة",
"size": "حجم الملف",
"source": "المصدر",
"subject": "الموضوع"
"subject": "الموضوع",
"viewEmbeddedEmail": "عرض البريد الإلكتروني المضمن"
},
"auth": {
"areYouSureYouWantToLogOut": "هل أنت متأكد أنك تريد تسجيل الخروج؟",
@@ -546,6 +547,7 @@
"attachments": "المرفقات",
"bcc": "نسخة مخفية",
"cc": "نسخة",
"clickToDownload": "انقر للتنزيل",
"date": "التاريخ",
"delete": "حذف",
"download": "تحميل",
+3 -1
View File
@@ -375,7 +375,8 @@
"showDuplicates": "Vis dubletter",
"size": "Filstørrelse",
"source": "Kilde",
"subject": "Emne"
"subject": "Emne",
"viewEmbeddedEmail": "Vis indlejret e-mail"
},
"auth": {
"areYouSureYouWantToLogOut": "Er du sikker på, du vil logge ud?",
@@ -546,6 +547,7 @@
"attachments": "Vedhæftninger",
"bcc": "Blindkopi",
"cc": "Kopi",
"clickToDownload": "Klik for at downloade",
"date": "Dato",
"delete": "Slet",
"download": "Download",
+3 -1
View File
@@ -375,7 +375,8 @@
"showDuplicates": "Duplikate anzeigen",
"size": "Dateigröße",
"source": "Quelle",
"subject": "Betreff"
"subject": "Betreff",
"viewEmbeddedEmail": "Eingebettete E-Mail anzeigen"
},
"auth": {
"areYouSureYouWantToLogOut": "Sind Sie sicher, dass Sie sich abmelden möchten?",
@@ -546,6 +547,7 @@
"attachments": "Anhänge",
"bcc": "BCC",
"cc": "CC",
"clickToDownload": "Zum Herunterladen klicken",
"date": "Datum",
"delete": "Löschen",
"download": "Herunterladen",
+3 -1
View File
@@ -375,7 +375,8 @@
"showDuplicates": "Show duplicates",
"size": "File size",
"source": "Source",
"subject": "Subject"
"subject": "Subject",
"viewEmbeddedEmail": "View embedded email"
},
"auth": {
"areYouSureYouWantToLogOut": "Are you sure you want to log out?",
@@ -546,6 +547,7 @@
"attachments": "Attachments",
"bcc": "BCC",
"cc": "CC",
"clickToDownload": "Click to download",
"date": "Date",
"delete": "Delete",
"download": "Download",
+3 -1
View File
@@ -375,7 +375,8 @@
"showDuplicates": "Mostrar duplicados",
"size": "Tamaño del archivo",
"source": "Fuente",
"subject": "Asunto"
"subject": "Asunto",
"viewEmbeddedEmail": "Ver correo electrónico incrustado"
},
"auth": {
"areYouSureYouWantToLogOut": "¿Estás seguro de que quieres cerrar sesión?",
@@ -546,6 +547,7 @@
"attachments": "Adjuntos",
"bcc": "CCO",
"cc": "CC",
"clickToDownload": "Hacer clic para descargar",
"date": "Fecha",
"delete": "Eliminar",
"download": "Descargar",
+3 -1
View File
@@ -375,7 +375,8 @@
"showDuplicates": "Näytä kaksoiskappaleet",
"size": "Tiedostokoko",
"source": "Lähde",
"subject": "Aihe"
"subject": "Aihe",
"viewEmbeddedEmail": "Näytä upotettu sähköposti"
},
"auth": {
"areYouSureYouWantToLogOut": "Oletko varma, että haluat kirjautua ulos?",
@@ -546,6 +547,7 @@
"attachments": "Liitteet",
"bcc": "Piilokopio",
"cc": "Kopio",
"clickToDownload": "Napsauta ladataksesi",
"date": "Päivämäärä",
"delete": "Poista",
"download": "Lataa",
+3 -1
View File
@@ -375,7 +375,8 @@
"showDuplicates": "Afficher les doublons",
"size": "Taille du fichier",
"source": "Source",
"subject": "Objet"
"subject": "Objet",
"viewEmbeddedEmail": "Voir l'e-mail intégré"
},
"auth": {
"areYouSureYouWantToLogOut": "Êtes-vous sûr de vouloir vous déconnecter ?",
@@ -546,6 +547,7 @@
"attachments": "Pièces jointes",
"bcc": "Cci",
"cc": "Cc",
"clickToDownload": "Cliquer pour télécharger",
"date": "Date",
"delete": "Supprimer",
"download": "Télécharger",
+3 -1
View File
@@ -375,7 +375,8 @@
"showDuplicates": "Mostra duplicati",
"size": "Dimensione file",
"source": "Origine",
"subject": "Oggetto"
"subject": "Oggetto",
"viewEmbeddedEmail": "Visualizza email incorporata"
},
"auth": {
"areYouSureYouWantToLogOut": "Sei sicuro di voler uscire?",
@@ -546,6 +547,7 @@
"attachments": "Allegati",
"bcc": "BCC",
"cc": "CC",
"clickToDownload": "Clicca per scaricare",
"date": "Data",
"delete": "Elimina",
"download": "Scarica",
+3 -1
View File
@@ -375,7 +375,8 @@
"showDuplicates": "重複ファイルを表示",
"size": "ファイルサイズ",
"source": "ソース",
"subject": "件名"
"subject": "件名",
"viewEmbeddedEmail": "埋め込みメールを表示"
},
"auth": {
"areYouSureYouWantToLogOut": "ログアウトしてもよろしいですか?",
@@ -546,6 +547,7 @@
"attachments": "添付ファイル",
"bcc": "BCC",
"cc": "CC",
"clickToDownload": "クリックしてダウンロード",
"date": "日付",
"delete": "削除",
"download": "ダウンロード",
+3 -1
View File
@@ -375,7 +375,8 @@
"showDuplicates": "중복 파일 표시",
"size": "파일 크기",
"source": "출처",
"subject": "제목"
"subject": "제목",
"viewEmbeddedEmail": "포함된 메일 보기"
},
"auth": {
"areYouSureYouWantToLogOut": "정말로 로그아웃하시겠습니까?",
@@ -546,6 +547,7 @@
"attachments": "첨부 파일",
"bcc": "숨은 참조",
"cc": "참조",
"clickToDownload": "클릭하여 다운로드",
"date": "날짜",
"delete": "삭제",
"download": "다운로드",
+3 -1
View File
@@ -375,7 +375,8 @@
"showDuplicates": "Duplicaten weergeven",
"size": "Bestandsgrootte",
"source": "Bron",
"subject": "Onderwerp"
"subject": "Onderwerp",
"viewEmbeddedEmail": "Ingesloten e-mail bekijken"
},
"auth": {
"areYouSureYouWantToLogOut": "Weet u zeker dat u wilt uitloggen?",
@@ -546,6 +547,7 @@
"attachments": "Bijlagen",
"bcc": "BCC",
"cc": "CC",
"clickToDownload": "Klik om te downloaden",
"date": "Datum",
"delete": "Verwijderen",
"download": "Downloaden",
+3 -1
View File
@@ -375,7 +375,8 @@
"showDuplicates": "Vis duplikater",
"size": "Filstørrelse",
"source": "Kilde",
"subject": "Emne"
"subject": "Emne",
"viewEmbeddedEmail": "Vis innebygd e-post"
},
"auth": {
"areYouSureYouWantToLogOut": "Er du sikker på at du vil logge ut?",
@@ -546,6 +547,7 @@
"attachments": "Vedlegg",
"bcc": "Blindkopi",
"cc": "Kopi",
"clickToDownload": "Klikk for å laste ned",
"date": "Dato",
"delete": "Slett",
"download": "Last ned",
+3 -1
View File
@@ -375,7 +375,8 @@
"showDuplicates": "Pokaż duplikaty",
"size": "Rozmiar pliku",
"source": "Źródło",
"subject": "Temat"
"subject": "Temat",
"viewEmbeddedEmail": "Wyświetl osadzony e-mail"
},
"auth": {
"areYouSureYouWantToLogOut": "Czy na pewno chcesz się wylogować?",
@@ -546,6 +547,7 @@
"attachments": "Załączniki",
"bcc": "UDW",
"cc": "DW",
"clickToDownload": "Kliknij, aby pobrać",
"date": "Data",
"delete": "Usuń",
"download": "Pobierz",
+3 -1
View File
@@ -375,7 +375,8 @@
"showDuplicates": "Mostrar duplicados",
"size": "Tamanho do arquivo",
"source": "Origem",
"subject": "Assunto"
"subject": "Assunto",
"viewEmbeddedEmail": "Ver e-mail incorporado"
},
"auth": {
"areYouSureYouWantToLogOut": "Tem certeza que deseja sair?",
@@ -546,6 +547,7 @@
"attachments": "Anexos",
"bcc": "BCC",
"cc": "CC",
"clickToDownload": "Clique para baixar",
"date": "Data",
"delete": "Excluir",
"download": "Baixar",
+3 -1
View File
@@ -375,7 +375,8 @@
"showDuplicates": "Показать дубликаты",
"size": "Размер файла",
"source": "Источник",
"subject": "Тема"
"subject": "Тема",
"viewEmbeddedEmail": "Просмотреть вложенное письмо"
},
"auth": {
"areYouSureYouWantToLogOut": "Вы уверены, что хотите выйти?",
@@ -546,6 +547,7 @@
"attachments": "Вложения",
"bcc": "Скрытая",
"cc": "Копия",
"clickToDownload": "Нажмите, чтобы скачать",
"date": "Дата",
"delete": "Удалить",
"download": "Скачать",
+3 -1
View File
@@ -375,7 +375,8 @@
"showDuplicates": "Visa dubbletter",
"size": "Filstorlek",
"source": "Källa",
"subject": "Ämne"
"subject": "Ämne",
"viewEmbeddedEmail": "Visa inbäddat e-postmeddelande"
},
"auth": {
"areYouSureYouWantToLogOut": "Är du säker på att du vill logga ut?",
@@ -546,6 +547,7 @@
"attachments": "Bilagor",
"bcc": "Hemlig kopia",
"cc": "Kopia",
"clickToDownload": "Klicka för att ladda ner",
"date": "Datum",
"delete": "Ta bort",
"download": "Ladda ner",
+3 -1
View File
@@ -375,7 +375,8 @@
"showDuplicates": "顯示重複檔案",
"size": "檔案大小",
"source": "來源",
"subject": "主旨"
"subject": "主旨",
"viewEmbeddedEmail": "檢視內嵌郵件"
},
"auth": {
"areYouSureYouWantToLogOut": "確定要登出嗎?",
@@ -546,6 +547,7 @@
"attachments": "附件",
"bcc": "密件副本 (BCC)",
"cc": "副本 (CC)",
"clickToDownload": "點擊下載",
"date": "日期",
"delete": "刪除",
"download": "下載",
+3 -1
View File
@@ -375,7 +375,8 @@
"showDuplicates": "显示重复文件",
"size": "文件大小",
"source": "来源",
"subject": "主题"
"subject": "主题",
"viewEmbeddedEmail": "查看内嵌邮件"
},
"auth": {
"areYouSureYouWantToLogOut": "您确定要退出登录吗?",
@@ -546,6 +547,7 @@
"attachments": "附件",
"bcc": "密送",
"cc": "抄送",
"clickToDownload": "点击下载",
"date": "日期",
"delete": "删除",
"download": "下载",