Keep the share email's button link within what mail clients tolerate

The "Open Puter" link names every item in the mail, capped at twenty. But
a single item's parameter is ~150 characters once the owner, uuid and an
encoded name are in it, so twenty of them run to several kilobytes — past
the ~2000 characters where older mail clients cut a URL off or stop making
it clickable — and the count alone couldn't do what its comment promised
for the mail's primary button.

Add a length budget beside the count: items go in, in digest order, only
while the whole link stays under it, so the button always works and the
first items are the ones highlighted. A single item always fits.
This commit is contained in:
Nariman Jelveh
2026-08-24 15:59:15 -07:00
parent 89f19e7b14
commit 3f9dc7c030
2 changed files with 49 additions and 15 deletions
@@ -22,6 +22,7 @@ import {
maskedSharePath,
ownerFromSharePath,
SHARE_DEEP_LINK_ITEMS_LIMIT,
SHARE_DEEP_LINK_MAX_LENGTH,
shareDeepLink,
sharedViewLink,
shareTargetLink,
@@ -137,6 +138,26 @@ describe('sharedViewLink', () => {
).searchParams.getAll('shared');
expect(shared).toEqual(paths.slice(0, SHARE_DEEP_LINK_ITEMS_LIMIT));
});
// Twenty ordinary names already run to several kilobytes once encoded, so
// the count alone is no guard; the link itself has to stay short enough.
it('stops adding items before the link outgrows what mail clients tolerate', () => {
const paths = Array.from(
{ length: SHARE_DEEP_LINK_ITEMS_LIMIT },
(_, i) => `/alice/${UID}/${'quarterly report '.repeat(8)}${i}.pdf`,
);
const link = sharedViewLink('https://puter.com', paths);
expect(link.length).toBeLessThanOrEqual(SHARE_DEEP_LINK_MAX_LENGTH);
const shared = new URL(link).searchParams.getAll('shared');
// Only a leading run made it — the first items, none skipped.
expect(shared.length).toBeGreaterThan(1);
expect(shared.length).toBeLessThan(paths.length);
expect(shared).toEqual(paths.slice(0, shared.length));
// The next item would not have fit.
expect(
link.length + `&shared=${encodeURIComponent(paths[shared.length])}`.length,
).toBeGreaterThan(SHARE_DEEP_LINK_MAX_LENGTH);
});
});
describe('shareTargetLink', () => {
+28 -15
View File
@@ -54,12 +54,20 @@ export const maskedSharePath = (target: ShareTarget): string | null => {
};
/**
* Items one link will highlight. Past this the link still opens Shared, just
* without picking the rest out — a query string of several kilobytes is where
* mail clients start truncating or refusing to make it clickable.
* Items one link will highlight, at most. Past this the link still opens
* Shared, just without picking the rest out.
*/
export const SHARE_DEEP_LINK_ITEMS_LIMIT = 20;
/**
* How long a link may run, in characters. Somewhere past two thousand, older
* mail clients cut a URL off or stop making it clickable — and this is the
* button — so items are added only while the whole link stays within this. A
* single item always fits: a username, a uuid and a filename come to a few
* hundred, and it takes long names for the count above to matter first.
*/
export const SHARE_DEEP_LINK_MAX_LENGTH = 2000;
/**
* A link that opens the recipient's Shared view with `paths` highlighted, once
* they are signed in. Only masked paths travel — each one's second segment is
@@ -67,18 +75,23 @@ export const SHARE_DEEP_LINK_ITEMS_LIMIT = 20;
* with the first. With no paths the link still lands on Shared.
*/
export const sharedViewLink = (origin: string, paths: string[]): string => {
const base = origin.replace(/\/+$/, '');
const unique = [...new Set(paths)].slice(0, SHARE_DEEP_LINK_ITEMS_LIMIT);
const query =
unique.length === 0
? `${SHARE_DEEP_LINK_PARAM}=`
: unique
.map(
(path) =>
`${SHARE_DEEP_LINK_PARAM}=${encodeURIComponent(path)}`,
)
.join('&');
return `${base}/?${query}`;
const base = `${origin.replace(/\/+$/, '')}/?`;
// The first items that fit, in order — never a later one over an
// earlier, so what is highlighted reads as the top of the list.
const params: string[] = [];
let length = base.length;
for (const path of new Set(paths)) {
if (params.length === SHARE_DEEP_LINK_ITEMS_LIMIT) break;
const param = `${SHARE_DEEP_LINK_PARAM}=${encodeURIComponent(path)}`;
const added = param.length + (params.length === 0 ? 0 : '&'.length);
if (length + added > SHARE_DEEP_LINK_MAX_LENGTH) break;
params.push(param);
length += added;
}
return (
base +
(params.length === 0 ? `${SHARE_DEEP_LINK_PARAM}=` : params.join('&'))
);
};
/** A link that opens `path`: the Shared view with that one item highlighted. */