fix: include original attachments when forwarding an email #214
This commit is contained in:
@@ -1819,7 +1819,8 @@ export default function Home() {
|
|||||||
subject: selectedEmail.subject,
|
subject: selectedEmail.subject,
|
||||||
body: selectedEmail.bodyValues?.[selectedEmail.textBody?.[0]?.partId || '']?.value || selectedEmail.preview || '',
|
body: selectedEmail.bodyValues?.[selectedEmail.textBody?.[0]?.partId || '']?.value || selectedEmail.preview || '',
|
||||||
htmlBody: selectedEmail.bodyValues?.[selectedEmail.htmlBody?.[0]?.partId || '']?.value || undefined,
|
htmlBody: selectedEmail.bodyValues?.[selectedEmail.htmlBody?.[0]?.partId || '']?.value || undefined,
|
||||||
receivedAt: selectedEmail.receivedAt
|
receivedAt: selectedEmail.receivedAt,
|
||||||
|
attachments: selectedEmail.attachments,
|
||||||
} : undefined)}
|
} : undefined)}
|
||||||
initialDraftText={composerDraftText}
|
initialDraftText={composerDraftText}
|
||||||
initialData={pendingDraft}
|
initialData={pendingDraft}
|
||||||
|
|||||||
@@ -86,9 +86,21 @@ interface EmailComposerProps {
|
|||||||
htmlBody?: string;
|
htmlBody?: string;
|
||||||
receivedAt?: string;
|
receivedAt?: string;
|
||||||
accountId?: string;
|
accountId?: string;
|
||||||
|
attachments?: Array<{ blobId: string; name?: string; type: string; size: number; cid?: string; disposition?: string }>;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type ComposerAttachment = {
|
||||||
|
file?: File;
|
||||||
|
name: string;
|
||||||
|
type: string;
|
||||||
|
size: number;
|
||||||
|
blobId?: string;
|
||||||
|
uploading?: boolean;
|
||||||
|
error?: boolean;
|
||||||
|
abortController?: AbortController;
|
||||||
|
};
|
||||||
|
|
||||||
export function EmailComposer({
|
export function EmailComposer({
|
||||||
onSend,
|
onSend,
|
||||||
onClose,
|
onClose,
|
||||||
@@ -201,7 +213,21 @@ export function EmailComposer({
|
|||||||
const [saveStatus, setSaveStatus] = useState<'idle' | 'saving' | 'saved' | 'error'>('idle');
|
const [saveStatus, setSaveStatus] = useState<'idle' | 'saving' | 'saved' | 'error'>('idle');
|
||||||
const saveTimeoutRef = useRef<NodeJS.Timeout | null>(null);
|
const saveTimeoutRef = useRef<NodeJS.Timeout | null>(null);
|
||||||
const lastSavedDataRef = useRef<string>("");
|
const lastSavedDataRef = useRef<string>("");
|
||||||
const [attachments, setAttachments] = useState<Array<{ file: File; blobId?: string; uploading?: boolean; error?: boolean; abortController?: AbortController }>>([]);
|
const [attachments, setAttachments] = useState<ComposerAttachment[]>(() => {
|
||||||
|
if (mode === 'forward' && replyTo?.attachments?.length) {
|
||||||
|
return replyTo.attachments
|
||||||
|
// Skip inline cid-referenced images — they're embedded in the forwarded HTML body
|
||||||
|
// (matches the viewer's hideInlineImageAttachments logic).
|
||||||
|
.filter(att => !(att.cid && att.disposition === 'inline' && (att.type || '').startsWith('image/')))
|
||||||
|
.map(att => ({
|
||||||
|
name: att.name || 'attachment',
|
||||||
|
type: att.type || 'application/octet-stream',
|
||||||
|
size: att.size,
|
||||||
|
blobId: att.blobId,
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
return [];
|
||||||
|
});
|
||||||
const inlineImagesRef = useRef<Array<{ cid: string; blobId: string; type: string; name: string; size: number; dataUrl: string }>>([]);
|
const inlineImagesRef = useRef<Array<{ cid: string; blobId: string; type: string; name: string; size: number; dataUrl: string }>>([]);
|
||||||
const fileInputRef = useRef<HTMLInputElement>(null);
|
const fileInputRef = useRef<HTMLInputElement>(null);
|
||||||
const [validationErrors, setValidationErrors] = useState<{ to?: boolean; subject?: boolean; body?: boolean }>({});
|
const [validationErrors, setValidationErrors] = useState<{ to?: boolean; subject?: boolean; body?: boolean }>({});
|
||||||
@@ -325,7 +351,7 @@ export function EmailComposer({
|
|||||||
stateRef.current = { to, cc, bcc, subject, body, showCc, showBcc, selectedIdentityId, subAddressTag, draftId };
|
stateRef.current = { to, cc, bcc, subject, body, showCc, showBcc, selectedIdentityId, subAddressTag, draftId };
|
||||||
|
|
||||||
// Track initial values for dirty detection (captured once on first render)
|
// Track initial values for dirty detection (captured once on first render)
|
||||||
const initialValuesRef = useRef({ to, cc, bcc, subject, body, attachmentCount: 0 });
|
const initialValuesRef = useRef({ to, cc, bcc, subject, body, attachmentCount: attachments.length });
|
||||||
const isDirtyRef = useRef(false);
|
const isDirtyRef = useRef(false);
|
||||||
isDirtyRef.current = to !== initialValuesRef.current.to || cc !== initialValuesRef.current.cc ||
|
isDirtyRef.current = to !== initialValuesRef.current.to || cc !== initialValuesRef.current.cc ||
|
||||||
bcc !== initialValuesRef.current.bcc || subject !== initialValuesRef.current.subject ||
|
bcc !== initialValuesRef.current.bcc || subject !== initialValuesRef.current.subject ||
|
||||||
@@ -524,9 +550,16 @@ export function EmailComposer({
|
|||||||
const addFiles = useCallback(async (files: File[]) => {
|
const addFiles = useCallback(async (files: File[]) => {
|
||||||
if (!client || files.length === 0) return;
|
if (!client || files.length === 0) return;
|
||||||
|
|
||||||
const newAttachments = files.map(file => {
|
const newAttachments: ComposerAttachment[] = files.map(file => {
|
||||||
const controller = new AbortController();
|
const controller = new AbortController();
|
||||||
return { file, uploading: true, abortController: controller };
|
return {
|
||||||
|
file,
|
||||||
|
name: file.name,
|
||||||
|
type: file.type || 'application/octet-stream',
|
||||||
|
size: file.size,
|
||||||
|
uploading: true,
|
||||||
|
abortController: controller,
|
||||||
|
};
|
||||||
});
|
});
|
||||||
setAttachments(prev => [...prev, ...newAttachments]);
|
setAttachments(prev => [...prev, ...newAttachments]);
|
||||||
|
|
||||||
@@ -669,9 +702,9 @@ export function EmailComposer({
|
|||||||
.filter(att => att.blobId && !att.uploading)
|
.filter(att => att.blobId && !att.uploading)
|
||||||
.map(att => ({
|
.map(att => ({
|
||||||
blobId: att.blobId!,
|
blobId: att.blobId!,
|
||||||
name: att.file.name,
|
name: att.name,
|
||||||
type: att.file.type,
|
type: att.type,
|
||||||
size: att.file.size,
|
size: att.size,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
// Create a hash of current data to compare with last saved
|
// Create a hash of current data to compare with last saved
|
||||||
@@ -910,16 +943,16 @@ export function EmailComposer({
|
|||||||
for (const att of attachments) {
|
for (const att of attachments) {
|
||||||
if (att.error || att.uploading) continue;
|
if (att.error || att.uploading) continue;
|
||||||
let content: ArrayBuffer;
|
let content: ArrayBuffer;
|
||||||
if (att.file.size > 0) {
|
if (att.file && att.file.size > 0) {
|
||||||
content = await att.file.arrayBuffer();
|
content = await att.file.arrayBuffer();
|
||||||
} else if (att.blobId && client) {
|
} else if (att.blobId && client) {
|
||||||
content = await client.fetchBlobArrayBuffer(att.blobId, att.file.name, att.file.type);
|
content = await client.fetchBlobArrayBuffer(att.blobId, att.name, att.type);
|
||||||
} else {
|
} else {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
mimeAttachments.push({
|
mimeAttachments.push({
|
||||||
filename: att.file.name,
|
filename: att.name,
|
||||||
contentType: att.file.type || 'application/octet-stream',
|
contentType: att.type || 'application/octet-stream',
|
||||||
content,
|
content,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -994,7 +1027,7 @@ export function EmailComposer({
|
|||||||
// Collect uploaded attachment blobIds for the send request
|
// Collect uploaded attachment blobIds for the send request
|
||||||
const uploadedAttachments: Array<{ blobId: string; name: string; type: string; size: number; disposition?: 'attachment' | 'inline'; cid?: string }> = attachments
|
const uploadedAttachments: Array<{ blobId: string; name: string; type: string; size: number; disposition?: 'attachment' | 'inline'; cid?: string }> = attachments
|
||||||
.filter(att => att.blobId && !att.uploading && !att.error)
|
.filter(att => att.blobId && !att.uploading && !att.error)
|
||||||
.map(att => ({ blobId: att.blobId!, name: att.file.name, type: att.file.type || 'application/octet-stream', size: att.file.size }));
|
.map(att => ({ blobId: att.blobId!, name: att.name, type: att.type || 'application/octet-stream', size: att.size }));
|
||||||
uploadedAttachments.push(...inlineAttachments);
|
uploadedAttachments.push(...inlineAttachments);
|
||||||
|
|
||||||
await onSend?.({
|
await onSend?.({
|
||||||
@@ -1375,9 +1408,9 @@ export function EmailComposer({
|
|||||||
) : (
|
) : (
|
||||||
<Paperclip className="w-3 h-3 flex-shrink-0" />
|
<Paperclip className="w-3 h-3 flex-shrink-0" />
|
||||||
)}
|
)}
|
||||||
<span className="max-w-[150px] md:max-w-[200px] truncate">{att.file.name}</span>
|
<span className="max-w-[150px] md:max-w-[200px] truncate">{att.name}</span>
|
||||||
<span className="text-xs text-muted-foreground whitespace-nowrap">
|
<span className="text-xs text-muted-foreground whitespace-nowrap">
|
||||||
({formatFileSize(att.file.size)})
|
({formatFileSize(att.size)})
|
||||||
</span>
|
</span>
|
||||||
<button
|
<button
|
||||||
onClick={() => removeAttachment(index)}
|
onClick={() => removeAttachment(index)}
|
||||||
|
|||||||
Reference in New Issue
Block a user