Both problems in #376 are real, and they have separate causes.
1. Adding or changing a contact photo always failed.
Cause: the editor uploaded the cropped image and saved media with a blobId. That is the JMAP extension RFC 9610 allows, but Stalwart refuses it with invalidProperties on media: "blobIds in media is not supported".
Checked against a live 0.16.22 server with a throwaway account, whose test objects were removed afterwards:
a blobId photo is refused with exactly the reported error;
a data:image/jpeg;base64,…uri is accepted, on create and on update, and returned unchanged;
a 134 KB one was accepted.
Fix:
The editor saves the 256 px JPEG it already produces as a data: URI, with no upload.
A new withPhoto() replaces or removes only the photo. Saving used to replace the whole media map, which would also have dropped a logo or sound.
Mock: it now refuses a blobId in media with Stalwart's error, so this can't pass tests again.
2. Avatars in the mail list were missing after a refresh.
Cause:Avatar returned nothing unless the contacts store had its cards loaded, and init() only loaded address books. Cards were loaded only by opening Contacts, composing, the address menu, or calendar birthdays. So a photo showed once one of those had happened, and was gone after the next reload.
The photo form wasn't the cause:contactPhoto already reads data: URIs.
Fix:
init() loads the cards in the background.
Avatar uses whatever cards are held, including cards from shared books.
A card's photo is resolved against the account the card belongs to (accountOfCard), in the avatar and in the contacts view, instead of always the reader's own account.
Adds none. npm run i18n:check reports the same counts as main.
Testing
New tests:
withPhoto: inline form with no blobId; replacing a photo while keeping a logo; removing only the photo; the result read back by contactPhoto.
contacts-cards-at-start.test.ts: init() loads cards, so lookupByEmail finds a sender.
In account.test.ts, through the proxy and mock: a blobId photo is refused with Stalwart's message, and a data: URI is accepted.
npm test passes: 1,340 web tests and 250 server tests. npm run typecheck is clean.
Checked in Chrome against npm run dev:mock:
Added a photo to Ada Lovelace through the editor's photo input. The save succeeded and the photo showed on the contact.
The card was stored with media as { kind: "photo", uri: "data:image/jpeg;base64,…", mediaType }.
Opened /mail/inbox fresh, without visiting Contacts. Every row from the contact's address ([email protected]) showed the photo as its avatar. Rows from [email protected], a different address that isn't on the card, correctly kept their initials.
## Summary
Both problems in #376 are real, and they have separate causes.
**1. Adding or changing a contact photo always failed.**
- **Cause:** the editor uploaded the cropped image and saved `media` with a `blobId`. That is the JMAP extension RFC 9610 allows, but Stalwart refuses it with `invalidProperties` on `media`: "blobIds in media is not supported".
- **Checked against a live 0.16.22 server** with a throwaway account, whose test objects were removed afterwards:
- a `blobId` photo is refused with exactly the reported error;
- a `data:image/jpeg;base64,…` `uri` is accepted, on create and on update, and returned unchanged;
- a 134 KB one was accepted.
- **Fix:**
- The editor saves the 256 px JPEG it already produces as a `data:` URI, with no upload.
- A new `withPhoto()` replaces or removes only the photo. Saving used to replace the whole `media` map, which would also have dropped a logo or sound.
- **Mock:** it now refuses a `blobId` in `media` with Stalwart's error, so this can't pass tests again.
**2. Avatars in the mail list were missing after a refresh.**
- **Cause:** `Avatar` returned nothing unless the contacts store had its cards loaded, and `init()` only loaded address books. Cards were loaded only by opening Contacts, composing, the address menu, or calendar birthdays. So a photo showed once one of those had happened, and was gone after the next reload.
- **The photo form wasn't the cause:** `contactPhoto` already reads `data:` URIs.
- **Fix:**
- `init()` loads the cards in the background.
- `Avatar` uses whatever cards are held, including cards from shared books.
- A card's photo is resolved against the account the card belongs to (`accountOfCard`), in the avatar and in the contacts view, instead of always the reader's own account.
**KNOWN-ISSUES.md** records the live result.
Fixes #376.
## Related issues
Fixes #376.
## Translations
Adds none. `npm run i18n:check` reports the same counts as `main`.
## Testing
- **New tests:**
- `withPhoto`: inline form with no `blobId`; replacing a photo while keeping a logo; removing only the photo; the result read back by `contactPhoto`.
- `contacts-cards-at-start.test.ts`: `init()` loads cards, so `lookupByEmail` finds a sender.
- In `account.test.ts`, through the proxy and mock: a `blobId` photo is refused with Stalwart's message, and a `data:` URI is accepted.
- `npm test` passes: 1,340 web tests and 250 server tests. `npm run typecheck` is clean.
- **Checked in Chrome against `npm run dev:mock`:**
1. Added a photo to Ada Lovelace through the editor's photo input. The save succeeded and the photo showed on the contact.
2. The card was stored with `media` as `{ kind: "photo", uri: "data:image/jpeg;base64,…", mediaType }`.
3. Opened `/mail/inbox` fresh, without visiting Contacts. Every row from the contact's address (`[email protected]`) showed the photo as its avatar. Rows from `[email protected]`, a different address that isn't on the card, correctly kept their initials.
**Merged** 2026-09-16 as coffey-labs/ihasmail@3d7602ce7474
<sub>Rebuilt from: git history, session transcript.</sub>
This repo is archived. You cannot comment on issues.
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
Summary
Both problems in #376 are real, and they have separate causes.
1. Adding or changing a contact photo always failed.
mediawith ablobId. That is the JMAP extension RFC 9610 allows, but Stalwart refuses it withinvalidPropertiesonmedia: "blobIds in media is not supported".blobIdphoto is refused with exactly the reported error;data:image/jpeg;base64,…uriis accepted, on create and on update, and returned unchanged;data:URI, with no upload.withPhoto()replaces or removes only the photo. Saving used to replace the wholemediamap, which would also have dropped a logo or sound.blobIdinmediawith Stalwart's error, so this can't pass tests again.2. Avatars in the mail list were missing after a refresh.
Avatarreturned nothing unless the contacts store had its cards loaded, andinit()only loaded address books. Cards were loaded only by opening Contacts, composing, the address menu, or calendar birthdays. So a photo showed once one of those had happened, and was gone after the next reload.contactPhotoalready readsdata:URIs.init()loads the cards in the background.Avataruses whatever cards are held, including cards from shared books.accountOfCard), in the avatar and in the contacts view, instead of always the reader's own account.KNOWN-ISSUES.md records the live result.
Fixes #376.
Related issues
Fixes #376.
Translations
Adds none.
npm run i18n:checkreports the same counts asmain.Testing
withPhoto: inline form with noblobId; replacing a photo while keeping a logo; removing only the photo; the result read back bycontactPhoto.contacts-cards-at-start.test.ts:init()loads cards, solookupByEmailfinds a sender.account.test.ts, through the proxy and mock: ablobIdphoto is refused with Stalwart's message, and adata:URI is accepted.npm testpasses: 1,340 web tests and 250 server tests.npm run typecheckis clean.npm run dev:mock:mediaas{ kind: "photo", uri: "data:image/jpeg;base64,…", mediaType }./mail/inboxfresh, without visiting Contacts. Every row from the contact's address ([email protected]) showed the photo as its avatar. Rows from[email protected], a different address that isn't on the card, correctly kept their initials.Merged 2026-09-16 as coffey-labs/ihasmail@3d7602ce74
Rebuilt from: git history, session transcript.