mirror of
https://github.com/bulwarkmail/webmail.git
synced 2026-10-31 00:12:39 +00:00
fix: show inline images whose part name is a path #1371
This commit is contained in:
@@ -2307,8 +2307,9 @@ export function EmailViewer({
|
||||
} else {
|
||||
URL.revokeObjectURL(objectUrl);
|
||||
}
|
||||
} catch {
|
||||
// Failed to fetch inline image, will show placeholder
|
||||
} catch (err) {
|
||||
// The image shows as the placeholder; say why, or the failure is invisible.
|
||||
console.warn(`Failed to fetch inline image cid:${cidValue}:`, err);
|
||||
}
|
||||
}));
|
||||
if (!cancelled) {
|
||||
|
||||
@@ -471,6 +471,15 @@ describe('JMAPClient resilience', () => {
|
||||
);
|
||||
});
|
||||
|
||||
it('keeps only the last path component of a name, so no %2F reaches the proxy (#1371)', async () => {
|
||||
const client = await connectWith({
|
||||
downloadUrl: 'https://mail.example.com/download/{accountId}/{blobId}/{name}?accept={type}',
|
||||
});
|
||||
expect(client.getBlobDownloadUrl('blob1', '/images/banner.jpg', 'image/jpeg', 'acct-1')).toBe(
|
||||
'https://mail.example.com/download/acct-1/blob1/banner.jpg?accept=image%2Fjpeg',
|
||||
);
|
||||
});
|
||||
|
||||
it('leaves an empty downloadUrl falsy so the unavailable guard still fires', async () => {
|
||||
const client = await connectWith({ downloadUrl: '' });
|
||||
expect(() => client.getBlobDownloadUrl('blob1')).toThrow('Download URL not available');
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { downloadNameSegment } from '../download-name';
|
||||
|
||||
describe('downloadNameSegment', () => {
|
||||
it.each([
|
||||
['banner.jpg', 'banner.jpg'],
|
||||
['/images/banner.jpg', 'banner.jpg'],
|
||||
['C:\\Users\\me\\report.pdf', 'report.pdf'],
|
||||
['100% done.txt', '100% done.txt'],
|
||||
])('turns %j into %j', (name, expected) => {
|
||||
expect(downloadNameSegment(name)).toBe(expected);
|
||||
});
|
||||
|
||||
it.each([undefined, '', ' ', '/images/', '\\'])('falls back to "download" for %j', (name) => {
|
||||
expect(downloadNameSegment(name)).toBe('download');
|
||||
});
|
||||
});
|
||||
+2
-1
@@ -33,6 +33,7 @@ import { buildEmailSort, compareEmails, hasKeywordLevels, hasTextLevels, type Ke
|
||||
import { emailMatchesLogin, sortIdentities } from "@/lib/identity-order";
|
||||
import type { AccountInfo } from "./types";
|
||||
import { stalwartChangeId } from "./stalwart-state";
|
||||
import { downloadNameSegment } from "./download-name";
|
||||
import { toInertBlob } from "@/lib/file-preview";
|
||||
import { toStorageQuota, type JMAPQuota, type StorageQuota } from "@/lib/quota";
|
||||
import { messagePriorityHeaders, type MessagePriority } from "@/lib/message-priority";
|
||||
@@ -5826,7 +5827,7 @@ export class JMAPClient implements IJMAPClient {
|
||||
return this.downloadUrl
|
||||
.replace('{accountId}', encodeURIComponent(accountId || this.accountId))
|
||||
.replace('{blobId}', encodeURIComponent(blobId))
|
||||
.replace('{name}', encodeURIComponent(name || 'download'))
|
||||
.replace('{name}', encodeURIComponent(downloadNameSegment(name)))
|
||||
.replace('{type}', encodeURIComponent(type || 'application/octet-stream'));
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
/**
|
||||
* The {name} segment of a JMAP download URL (RFC 8620 §6.2).
|
||||
*
|
||||
* The server only uses it for the Content-Disposition filename, but mailers
|
||||
* sometimes put a path in a part's name ("/images/banner.jpg"). Percent-
|
||||
* encoded, its slashes become %2F, which Apache (without AllowEncodedSlashes
|
||||
* NoDecode), Traefik and nginx proxy_pass with a URI reject or decode, so the
|
||||
* download failed and inline cid: images showed as blank (#1371). Keep the
|
||||
* last path component instead.
|
||||
*/
|
||||
export function downloadNameSegment(name: string | undefined): string {
|
||||
const base = (name ?? '').split(/[/\\]/).pop()?.trim();
|
||||
return base || 'download';
|
||||
}
|
||||
+2
-1
@@ -2,6 +2,7 @@ import { postJmap, fetchJmapSession, rebaseApiUrl, type JmapSessionDocument } fr
|
||||
import { fetchJmapServer } from '@/lib/stalwart/server-fetch';
|
||||
import { jmapError, type JmapErrorInfo } from '@/lib/jmap/jmap-error';
|
||||
import type { FileNode } from '@/lib/jmap/types';
|
||||
import { downloadNameSegment } from '@/lib/jmap/download-name';
|
||||
|
||||
/**
|
||||
* Server-side JMAP FileNode content I/O for the WOPI host (#425).
|
||||
@@ -148,7 +149,7 @@ export async function downloadFileBlob(
|
||||
const url = expandTemplate(downloadUrl, {
|
||||
accountId,
|
||||
blobId,
|
||||
name,
|
||||
name: downloadNameSegment(name),
|
||||
type: type || 'application/octet-stream',
|
||||
});
|
||||
return fetchJmapServer(url, {
|
||||
|
||||
Reference in New Issue
Block a user