Refactor: decouple email body and attachment storage

This commit is contained in:
rustmailer
2026-03-24 21:48:04 +08:00
parent c19f3977ba
commit a41b5417e3
31 changed files with 1630 additions and 1152 deletions
+1
View File
@@ -45,6 +45,7 @@ export interface EmailEnvelope {
size: number;
thread_id: string,
attachment_count: number;
regular_attachment_count: number;
tags: string[];
content_hash: string;
}
+11 -8
View File
@@ -34,16 +34,16 @@ export const get_thread_messages = async (accountId: number, thread_id: string,
return response.data;
}
export const download_attachment = async (accountId: number, id: string, attachmentFileName: string) => {
const response = await axiosInstance.get(`api/v1/download-attachment/${accountId}/${id}?name=${attachmentFileName}`, { responseType: 'blob' });
export const download_attachment = async (accountId: number, id: string, content_hash: string, fileName: string) => {
const response = await axiosInstance.get(`api/v1/download-attachment/${accountId}/${id}?content_hash=${content_hash}`, { responseType: 'blob' });
const blob = new Blob([response.data]);
saveAs(blob, attachmentFileName);
saveAs(blob, fileName);
};
export const download_nested_attachment = async (accountId: number, id: string, attachmentFileName: string, nestedAttachmentFileName: string) => {
const response = await axiosInstance.get(`api/v1/download-nested-attachment/${accountId}/${id}?name=${attachmentFileName}&nested_name=${nestedAttachmentFileName}`, { responseType: 'blob' });
export const download_nested_attachment = async (accountId: number, id: string, content_hash: string, nested_content_hash: 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, nestedAttachmentFileName);
saveAs(blob, nested_content_hash);
};
export interface AttachmentInfo {
/** MIME content type of the attachment (e.g., `image/png`, `application/pdf`). */
@@ -56,7 +56,10 @@ export interface AttachmentInfo {
filename: string;
/** Size of the attachment in bytes. */
size: number;
content_hash: string;
is_message: boolean
}
export interface MessageContentResponse {
text?: string;
html?: string;
@@ -84,8 +87,8 @@ export const load_message = async (accountId: number, id: string) => {
return response.data;
};
export const load_nested_message = async (accountId: number, id: string, attachmentFileName: string) => {
const response = await axiosInstance.get<NestedMessageContentResponse>(`api/v1/nested-message-content/${accountId}/${id}?name=${attachmentFileName}`);
export const load_nested_message = async (accountId: number, id: string, content_hash: string) => {
const response = await axiosInstance.get<NestedMessageContentResponse>(`api/v1/nested-message-content/${accountId}/${id}?content_hash=${content_hash}`);
return response.data;
};
+1 -1
View File
@@ -282,7 +282,7 @@ export function MailListTable({
{
id: "attachment_count",
header: () => <Paperclip size={16} />,
cell: ({ row }) => <span className='text-xs'>{row.original.attachment_count}</span>,
cell: ({ row }) => <span className='text-xs'>{row.original.regular_attachment_count}</span>,
meta: { className: 'text-left text-xs' },
minSize: 40,
maxSize: 40
+2 -2
View File
@@ -154,7 +154,7 @@ export function MailList({
)}
{items.map((item, index) => {
const hasAttachments = item.attachment_count > 0
const hasAttachments = item.regular_attachment_count > 0
const isSelectedRow = currentEnvelope?.id === item.id
const isChecked = hasSelected(item.account_id, item.id)
@@ -209,7 +209,7 @@ export function MailList({
{hasAttachments && (
<div className="flex items-center gap-1">
<Paperclip className="h-3 w-3" />
<span>{item.attachment_count}</span>
<span>{item.regular_attachment_count}</span>
</div>
)}
+13 -10
View File
@@ -126,13 +126,13 @@ export function MailMessageView({
const [attachments, setAttachments] = useState<AttachmentInfo[] | null>(null);
const [loading, setLoading] = useState(true);
const [downloadingAttachmentFileName, setDownloadingAttachmentFileName] = useState<string | null>(null);
const [nestedEmlFile, setNestedEmlFile] = useState<string | null>(null);
const [nestedEmlFile, setNestedEmlFile] = useState<AttachmentInfo | null>(null);
const { getEmailById } = useMinimalAccountList();
const [threadOpen, setThreadOpen] = useState(false);
const downloadAttachmentMutation = useMutation({
mutationFn: ({ fileName }: { fileName: string }) =>
download_attachment(envelope.account_id, envelope.id, fileName),
mutationFn: ({ content_hash }: { content_hash: string }) =>
download_attachment(envelope.account_id, envelope.id, content_hash, downloadingAttachmentFileName!),
onSuccess: () => setDownloadingAttachmentFileName(null),
onError: (error: any) => {
setDownloadingAttachmentFileName(null);
@@ -168,8 +168,8 @@ export function MailMessageView({
}, [envelope.id]);
const handleViewNestedEml = (filename: string) => {
setNestedEmlFile(filename);
const handleViewNestedEml = (attachment: AttachmentInfo) => {
setNestedEmlFile(attachment);
};
const toggleToDelete = (accountId: number, mailId: string) => {
@@ -317,7 +317,7 @@ export function MailMessageView({
<div className="space-y-2">
{nonInline.map((attachment, i) => {
const { icon, color } = getFileConfig(attachment.file_type);
const isNestedEmail = attachment.file_type.toLowerCase() === 'message/rfc822';
const is_message = attachment.is_message;
return <div key={i} className="flex items-center">
<div className="group flex items-center gap-2 p-1 hover:bg-muted/60 rounded transition-colors min-w-0 w-full">
@@ -337,14 +337,16 @@ export function MailMessageView({
</div>
</div>
<div className="flex items-center space-x-3 ml-auto pr-1">
{isNestedEmail && (
{is_message && (
<Tooltip>
<TooltipTrigger asChild>
<Button
variant="ghost"
size="sm"
className="h-7 w-7 p-0 text-orange-600 hover:text-orange-700 hover:bg-orange-50"
onClick={() => handleViewNestedEml(attachment.filename)}
onClick={() => {
handleViewNestedEml(attachment);
}}
>
<MessageSquareMore className="h-4 w-4" />
</Button>
@@ -362,7 +364,7 @@ export function MailMessageView({
className="w-4 h-4 cursor-pointer"
onClick={() => {
setDownloadingAttachmentFileName(attachment.filename);
downloadAttachmentMutation.mutate({ fileName: attachment.filename });
downloadAttachmentMutation.mutate({ content_hash: attachment.content_hash });
}}
/>
)}
@@ -407,7 +409,8 @@ export function MailMessageView({
onOpenChange={(open: boolean) => !open && setNestedEmlFile(null)}
accountId={envelope.account_id}
envelopeId={envelope.id}
fileName={nestedEmlFile || ''}
fileName={nestedEmlFile?.filename || ''}
content_hash={nestedEmlFile?.content_hash}
/>
</div>
);
@@ -17,7 +17,7 @@ const MessageHeader = ({
}: {
envelope: EmailEnvelope,
attachments?: AttachmentInfo[],
onDownload: (fileName: string) => void
onDownload: (nested_content_hash: string) => void
}) => {
const { t } = useTranslation();
const displayAttachments = attachments || [];
@@ -100,7 +100,7 @@ const MessageHeader = ({
<Tooltip key={i}>
<TooltipTrigger asChild>
<button
onClick={() => onDownload(att.filename)}
onClick={() => onDownload(att.content_hash)}
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>
@@ -126,11 +126,12 @@ const MessageHeader = ({
export function NestedEmailDialog({ open, onOpenChange, accountId, envelopeId, fileName }: any) {
export function NestedEmailDialog({ open, onOpenChange, accountId, envelopeId, fileName, content_hash }: any) {
const { data, isLoading } = useQuery({
queryKey: ['nested-message', accountId, envelopeId, fileName],
queryFn: () => load_nested_message(accountId, envelopeId, fileName),
enabled: open && !!fileName,
queryKey: ['nested-message', accountId, envelopeId, content_hash],
queryFn: () => load_nested_message(accountId, envelopeId, content_hash),
enabled: open && !!content_hash,
});
return (
@@ -151,7 +152,7 @@ export function NestedEmailDialog({ open, onOpenChange, accountId, envelopeId, f
<MessageHeader
envelope={data.envelope}
attachments={data.attachments}
onDownload={(nestedFileName) => download_nested_attachment(accountId, envelopeId, fileName, nestedFileName)}
onDownload={(nested_content_hash) => download_nested_attachment(accountId, envelopeId, content_hash, nested_content_hash)}
/>
<div className="mt-8 pt-8 border-t border-slate-100">