mirror of
https://github.com/bulwarkmail/native.git
synced 2026-10-31 16:06:32 +00:00
fix: show inline images whose part name is a path
This commit is contained in:
@@ -69,6 +69,15 @@ describe('blob operations', () => {
|
|||||||
|
|
||||||
expect(url).toContain('my%20file%20(1).pdf');
|
expect(url).toContain('my%20file%20(1).pdf');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// A part named like a path: an encoded slash (%2F) in the URL path is
|
||||||
|
// refused or decoded by common reverse proxies, so inline images went
|
||||||
|
// blank (webmail #1371).
|
||||||
|
it('keeps only the last path component of a part name', () => {
|
||||||
|
expect(getDownloadUrl('b1', '/images/banner.jpg', 'image/jpeg')).toContain('/b1/banner.jpg?');
|
||||||
|
expect(getDownloadUrl('b1', 'C:\\pics\\logo.png', 'image/png')).toContain('/b1/logo.png?');
|
||||||
|
expect(getDownloadUrl('b1', 'dir/', 'image/png')).toContain('/b1/download?');
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -194,6 +194,8 @@ describe('JMAPClient hardening', () => {
|
|||||||
const session = await client.connect('https://mail.example.com', 'user', 'pass');
|
const session = await client.connect('https://mail.example.com', 'user', 'pass');
|
||||||
expect(session.apiUrl).toBe('https://mail.example.com/jmap/');
|
expect(session.apiUrl).toBe('https://mail.example.com/jmap/');
|
||||||
expect(client.getBlobDownloadUrl('b1', 'n')).toBe('https://mail.example.com/download/acc-1/b1/n?type=application%2Foctet-stream');
|
expect(client.getBlobDownloadUrl('b1', 'n')).toBe('https://mail.example.com/download/acc-1/b1/n?type=application%2Foctet-stream');
|
||||||
|
// A part name that is a path keeps its last component (webmail #1371).
|
||||||
|
expect(client.getBlobDownloadUrl('b1', '/images/n.jpg')).toBe('https://mail.example.com/download/acc-1/b1/n.jpg?type=application%2Foctet-stream');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('refetches the final URL when a redirect dropped the Authorization header', async () => {
|
it('refetches the final URL when a redirect dropped the Authorization header', async () => {
|
||||||
|
|||||||
+2
-1
@@ -2,6 +2,7 @@ import { File } from 'expo-file-system';
|
|||||||
import type * as LegacyFileSystemTypes from 'expo-file-system/legacy';
|
import type * as LegacyFileSystemTypes from 'expo-file-system/legacy';
|
||||||
import { jmapClient } from './jmap-client';
|
import { jmapClient } from './jmap-client';
|
||||||
import { batched } from './jmap-result';
|
import { batched } from './jmap-result';
|
||||||
|
import { downloadNameSegment } from './download-name';
|
||||||
import { CAPABILITIES } from './types';
|
import { CAPABILITIES } from './types';
|
||||||
import { getClientCertAlias, secureFetch } from '../lib/client-cert';
|
import { getClientCertAlias, secureFetch } from '../lib/client-cert';
|
||||||
|
|
||||||
@@ -268,7 +269,7 @@ export function getDownloadUrl(
|
|||||||
return session.downloadUrl
|
return session.downloadUrl
|
||||||
.replace('{accountId}', encodeURIComponent(accountId ?? jmapClient.accountId))
|
.replace('{accountId}', encodeURIComponent(accountId ?? jmapClient.accountId))
|
||||||
.replace('{blobId}', encodeURIComponent(blobId))
|
.replace('{blobId}', encodeURIComponent(blobId))
|
||||||
.replace('{name}', encodeURIComponent(name ?? 'download'))
|
.replace('{name}', encodeURIComponent(downloadNameSegment(name)))
|
||||||
.replace('{type}', encodeURIComponent(type ?? 'application/octet-stream'));
|
.replace('{type}', encodeURIComponent(type ?? 'application/octet-stream'));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,15 @@
|
|||||||
|
/**
|
||||||
|
* The {name} segment of a JMAP download URL (RFC 8620 §6.2). Port of the
|
||||||
|
* webmail's lib/jmap/download-name.ts (#1371).
|
||||||
|
*
|
||||||
|
* 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. Keep the last path
|
||||||
|
* component instead.
|
||||||
|
*/
|
||||||
|
export function downloadNameSegment(name: string | undefined): string {
|
||||||
|
const base = (name ?? '').split(/[/\\]/).pop()?.trim();
|
||||||
|
return base || 'download';
|
||||||
|
}
|
||||||
@@ -15,6 +15,7 @@ import {
|
|||||||
type OAuthTokenSource,
|
type OAuthTokenSource,
|
||||||
} from '../lib/oauth';
|
} from '../lib/oauth';
|
||||||
import { FirstTouchGate } from './first-touch-gate';
|
import { FirstTouchGate } from './first-touch-gate';
|
||||||
|
import { downloadNameSegment } from './download-name';
|
||||||
import { writeSharedSecret, moveToSharedGroup } from '../lib/ios-push-shared';
|
import { writeSharedSecret, moveToSharedGroup } from '../lib/ios-push-shared';
|
||||||
import { beginOwnWrite, recordOwnEmailWrites } from './own-writes';
|
import { beginOwnWrite, recordOwnEmailWrites } from './own-writes';
|
||||||
|
|
||||||
@@ -1347,7 +1348,7 @@ export class JMAPClient {
|
|||||||
return this.session.downloadUrl
|
return this.session.downloadUrl
|
||||||
.replace('{accountId}', encodeURIComponent(accountId ?? this.accountId))
|
.replace('{accountId}', encodeURIComponent(accountId ?? this.accountId))
|
||||||
.replace('{blobId}', encodeURIComponent(blobId))
|
.replace('{blobId}', encodeURIComponent(blobId))
|
||||||
.replace('{name}', encodeURIComponent(name || 'download'))
|
.replace('{name}', encodeURIComponent(downloadNameSegment(name)))
|
||||||
.replace('{type}', encodeURIComponent(type || 'application/octet-stream'));
|
.replace('{type}', encodeURIComponent(type || 'application/octet-stream'));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user