mirror of
https://github.com/bulwarkmail/native.git
synced 2026-11-04 00:06:53 +00:00
Stalwart 1.0 returns every photo, logo and sound in a contact card as a blobId without a uri, including photos saved as data: URIs. The app read only `uri`, so sender avatars, the contact list, the detail screen, the form preview and vCard export showed nothing. Worse, the form rebuilt `media` from the card on every save and sent it without the photo, or as `null`, so editing a phone number deleted the photo on the server. - `ContactMedia` carries `blobId`, `label`, `pref` and `contexts`, and `uri` is optional. The photo shown is the photo entry with the lowest `pref`, as a URI (0.16) or a blob (1.0). - Blob photos load from the download URL with the Authorization header, like file thumbnails. At most half of the server's `maxConcurrentRequests` load at once, a failed one is tried once more and then left alone for two minutes, and one that loaded skips the queue from then on. - The form leaves `media` out of the save unless the photo changed. A changed photo is built on the card as the server has it now, so a stale blobId can't make 1.0 refuse the update, and a new photo keeps the key, `pref`, `label` and `contexts` of the one it replaces. - A new photo is uploaded into the user's own account and sent as a blobId where the blob capability lists ContactCard (1.0). Otherwise it goes as a data: URI, and a refusal like 0.16's "blobIds in media is not supported." retries once with the data: URI and is remembered for the account. - vCard export fetches blob photos with batched Blob/get requests and writes them inline. It also writes photos whose data: URI names no media type, which it dropped before. Duplicate and moving a card to another account send the bytes instead of the source's blobIds. - An update never clears `id`, `uid` or `addressBookIds`, and vCard export writes MEMBER values that are already URIs as they are instead of `urn:uuid:urn:uuid:…`. The form's state and patch building moved to `src/lib/contact-form.ts` so the save path can be tested.
47 lines
1.7 KiB
TypeScript
47 lines
1.7 KiB
TypeScript
// base64 data: URIs, as contact photos carry them (webmail lib/data-uri.ts).
|
|
//
|
|
// Stalwart writes three shapes: `data:<type>;base64,…`, `data:;base64,…`
|
|
// (0.16.x) and `data:base64,…` (0.16.0, no parameter separator). The last two
|
|
// carry no media type, so it comes from the card's `mediaType` when known.
|
|
|
|
/** The media type (if any) and base64 payload of a base64 data: URI. */
|
|
export function parseBase64DataUri(uri: string): { type?: string; data: string } | null {
|
|
if (!uri.toLowerCase().startsWith('data:')) return null;
|
|
const comma = uri.indexOf(',');
|
|
if (comma < 0) return null;
|
|
const header = uri.slice(5, comma).split(';');
|
|
if (header[header.length - 1].trim().toLowerCase() !== 'base64') return null;
|
|
const type = header.length > 1 ? header[0].trim() : '';
|
|
return { type: type || undefined, data: uri.slice(comma + 1) };
|
|
}
|
|
|
|
export function bytesToBase64(bytes: Uint8Array): string {
|
|
let binary = '';
|
|
const chunk = 0x8000;
|
|
for (let i = 0; i < bytes.length; i += chunk) {
|
|
binary += String.fromCharCode.apply(null, Array.from(bytes.subarray(i, i + chunk)));
|
|
}
|
|
return btoa(binary);
|
|
}
|
|
|
|
/**
|
|
* The bytes of a base64 data: URI with their media type; `fallbackType`
|
|
* applies when the URI names none. Null for anything else.
|
|
*/
|
|
export function dataUriToBytes(
|
|
uri: string,
|
|
fallbackType?: string,
|
|
): { bytes: Uint8Array; type: string } | null {
|
|
const parsed = parseBase64DataUri(uri);
|
|
if (!parsed) return null;
|
|
let binary: string;
|
|
try {
|
|
binary = atob(parsed.data);
|
|
} catch {
|
|
return null;
|
|
}
|
|
const bytes = new Uint8Array(binary.length);
|
|
for (let i = 0; i < binary.length; i++) bytes[i] = binary.charCodeAt(i);
|
|
return { bytes, type: parsed.type || fallbackType || 'application/octet-stream' };
|
|
}
|