mirror of
https://github.com/rustmailer/bichon.git
synced 2026-08-03 07:48:34 +02:00
Refactor: decouple email body and attachment storage
This commit is contained in:
@@ -45,6 +45,7 @@ export interface EmailEnvelope {
|
||||
size: number;
|
||||
thread_id: string,
|
||||
attachment_count: number;
|
||||
regular_attachment_count: number;
|
||||
tags: string[];
|
||||
content_hash: string;
|
||||
}
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
)}
|
||||
|
||||
|
||||
@@ -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">
|
||||
|
||||
Reference in New Issue
Block a user