mirror of
https://github.com/bulwarkmail/webmail.git
synced 2026-10-30 16:16:19 +00:00
Stalwart 1.0 returns every photo, logo and sound in a contact card as a blobId without a uri, including every photo Bulwark saved as a data: URI. Bulwark read only `uri`, so avatars, the contact list and detail, print, vCard export and the "has photo" filter showed nothing, and the form sent `media` without the photo on every save, so editing a phone number deleted the photo on the server. - `ContactMedia` carries `blobId`, `label`, `pref` and `contexts`; `uri` is optional, and `getContactPhotoRef`/`hasContactPhoto` read both. - Blob photos are downloaded once each, at most two at a time and at a paced rate, only while on screen, into an LRU of object URLs revoked on eviction, account switch and logout. They share Stalwart's per-account budget of 4 concurrent requests and 1000 a minute. - Avatars look senders up in an index built once per contact list. - The form leaves `media` out unless the photo changed, and otherwise passes the other entries through as the server returned them. - A new photo is uploaded into the user's own account and sent as a blobId where the blob capability lists ContactCard (1.0). 0.16 refuses blobIds in media and keeps getting a data: URI; a refusal at runtime falls back the same way for the rest of the session. - Print and vCard export download blob media first; duplicate and cross-account move send the bytes instead of the source's blobIds. - vCard export also writes photos whose data: URI names no media type (`data:;base64,`, `data:base64,`), which it dropped before.
48 lines
2.0 KiB
TypeScript
48 lines
2.0 KiB
TypeScript
/**
|
|
* base64 data: URIs, as contact photos carry them.
|
|
*
|
|
* 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) };
|
|
}
|
|
|
|
/** The bytes of a base64 data: URI; `fallbackType` applies when the URI names none. */
|
|
export function dataUriToBlob(uri: string, fallbackType?: string): Blob | 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 new Blob([bytes], { type: parsed.type || fallbackType || 'application/octet-stream' });
|
|
}
|
|
|
|
/** A base64 data: URI of the blob, labelled `mediaType` when given, else the blob's own type. */
|
|
export function blobToDataUri(blob: Blob, mediaType?: string): Promise<string> {
|
|
return new Promise((resolve, reject) => {
|
|
const reader = new FileReader();
|
|
reader.onload = () => {
|
|
const result = String(reader.result);
|
|
const data = result.slice(result.indexOf(',') + 1);
|
|
resolve(`data:${mediaType || blob.type || 'application/octet-stream'};base64,${data}`);
|
|
};
|
|
reader.onerror = () => reject(reader.error ?? new Error('read-failed'));
|
|
reader.readAsDataURL(blob);
|
|
});
|
|
}
|