fix: detect previewable attachments by file extension when MIME type is octet-stream #309

This commit is contained in:
rustmailer
2026-06-27 16:08:42 +08:00
parent 875eb2e309
commit 5034760517
@@ -37,6 +37,43 @@ import { getFileConfig } from './mail-message-view';
const PREVIEWABLE_IMAGE = /^image\/(png|jpeg|gif|webp|svg\+xml)$/;
const PREVIEWABLE_TEXT = /^(text\/(plain|csv|html|xml|css|javascript|markdown)|application\/(json|xml|javascript|x-httpd-php|x-sh|x-perl|x-python|x-ruby))$/;
const EXT_TO_MIME: Record<string, string> = {
'.pdf': 'application/pdf',
'.png': 'image/png',
'.jpg': 'image/jpeg',
'.jpeg': 'image/jpeg',
'.gif': 'image/gif',
'.webp': 'image/webp',
'.svg': 'image/svg+xml',
'.txt': 'text/plain',
'.csv': 'text/csv',
'.html': 'text/html',
'.htm': 'text/html',
'.xml': 'application/xml',
'.css': 'text/css',
'.js': 'application/javascript',
'.mjs': 'application/javascript',
'.md': 'text/markdown',
'.json': 'application/json',
'.php': 'application/x-httpd-php',
'.sh': 'application/x-sh',
'.pl': 'application/x-perl',
'.py': 'application/x-python',
'.rb': 'application/x-ruby',
};
function getExtensionMime(filename: string): string | null {
const dot = filename.lastIndexOf('.');
if (dot < 0) return null;
const ext = filename.slice(dot).toLowerCase();
return EXT_TO_MIME[ext] ?? null;
}
function resolveContentType(contentType: string, fileName: string): string {
if (contentType && contentType !== 'application/octet-stream') return contentType;
return getExtensionMime(fileName) ?? contentType;
}
export interface PreviewAttachment {
content_hash: string;
file_type: string;
@@ -57,16 +94,16 @@ interface AttachmentPreviewProps {
attachmentIndex?: number;
}
function isImagePreview(contentType: string) {
return PREVIEWABLE_IMAGE.test(contentType);
function isImagePreview(contentType: string, fileName?: string) {
return PREVIEWABLE_IMAGE.test(resolveContentType(contentType, fileName ?? ''));
}
function isPdfPreview(contentType: string) {
return contentType === 'application/pdf';
function isPdfPreview(contentType: string, fileName?: string) {
return resolveContentType(contentType, fileName ?? '') === 'application/pdf';
}
function isTextPreview(contentType: string) {
return PREVIEWABLE_TEXT.test(contentType);
function isTextPreview(contentType: string, fileName?: string) {
return PREVIEWABLE_TEXT.test(resolveContentType(contentType, fileName ?? ''));
}
export default function AttachmentPreview({
@@ -90,7 +127,7 @@ export default function AttachmentPreview({
const imageIndices = useMemo(() => {
if (!attachments) return [];
return attachments
.map((a, i) => (isImagePreview(a.file_type) ? i : -1))
.map((a, i) => (isImagePreview(a.file_type, a.filename) ? i : -1))
.filter((i) => i >= 0);
}, [attachments]);
@@ -139,14 +176,16 @@ export default function AttachmentPreview({
return () => window.removeEventListener('keydown', handler);
}, [open, goPrev, goNext]);
const effectiveType = resolveContentType(resolved.contentType, resolved.fileName);
// ── Fetch preview blob ─────────────────────────────────────────
const previewMutation = useMutation({
mutationFn: () => preview_attachment(accountId, envelopeId, resolved.contentHash),
onSuccess: (blob) => {
if (isTextPreview(resolved.contentType)) {
if (isTextPreview(resolved.contentType, resolved.fileName)) {
blob.text().then(setTextContent);
} else {
const typedBlob = new Blob([blob], { type: resolved.contentType });
const typedBlob = new Blob([blob], { type: effectiveType });
setBlobUrl(URL.createObjectURL(typedBlob));
}
},
@@ -180,9 +219,9 @@ export default function AttachmentPreview({
const { icon } = useMemo(() => getFileConfig(resolved.contentType), [resolved.contentType]);
const isImage = isImagePreview(resolved.contentType);
const isPdf = isPdfPreview(resolved.contentType);
const isText = isTextPreview(resolved.contentType);
const isImage = isImagePreview(resolved.contentType, resolved.fileName);
const isPdf = isPdfPreview(resolved.contentType, resolved.fileName);
const isText = isTextPreview(resolved.contentType, resolved.fileName);
const showArrows = imageTotal > 1 && isImage;
return (