fix(email): populate mimeTree for JMAP accounts in Show Mail Structure (#92)

The JMAP body-fetch path never requested or stored `bodyStructure`, so
`body.mimeTree` was always null for JMAP accounts — causing Show Mail
Structure to show nothing.

Fix: include `bodyStructure` in the JMAP `Email/get` request and convert
it to the same JSON format used by the IMAP path via the new
`_jmapBodyStructureToJson` helper.  The parsed tree is persisted in the
DB and returned from `getEmailBody`, so the cached round-trip also works.

Tests added:
- Unit: JMAP getEmailBody populates mimeTree from bodyStructure and
  survives the cache round-trip; null when bodyStructure is absent.
- Widget: Show Mail Structure dialog displays all MIME parts when
  mimeTree is present; snackbar appears when mimeTree is null.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Thomas SharedInbox
2026-05-15 14:23:43 +02:00
co-authored by Claude Sonnet 4.6
parent 451aceaeed
commit ef3fb72f4e
3 changed files with 226 additions and 0 deletions
@@ -318,9 +318,17 @@ class EmailRepositoryImpl implements EmailRepository {
'htmlBody',
'bodyValues',
'attachments',
'bodyStructure',
],
'fetchHTMLBodyValues': true,
'fetchTextBodyValues': true,
'bodyProperties': [
'partId',
'type',
'name',
'size',
'subParts',
],
},
'0',
],
@@ -340,6 +348,12 @@ class EmailRepositoryImpl implements EmailRepository {
}).toList(),
);
final rawBodyStructure =
emailData['bodyStructure'] as Map<String, dynamic>?;
final mimeTreeJson = rawBodyStructure != null
? jsonEncode(_jmapBodyStructureToJson(rawBodyStructure))
: null;
await _db.into(_db.emailBodies).insertOnConflictUpdate(
EmailBodiesCompanion.insert(
emailId: emailId,
@@ -347,6 +361,7 @@ class EmailRepositoryImpl implements EmailRepository {
htmlBody: Value(htmlBody),
attachmentsJson: Value(attachmentsJson),
headersJson: Value(headersJson),
mimeTreeJson: Value(mimeTreeJson),
cachedAt: Value(DateTime.now()),
),
);
@@ -357,6 +372,7 @@ class EmailRepositoryImpl implements EmailRepository {
htmlBody: htmlBody,
attachments: _parseAttachments(attachmentsJson),
headers: _parseHeaders(headersJson),
mimeTree: _parseMimeTree(mimeTreeJson),
);
}
@@ -3018,3 +3034,16 @@ Map<String, dynamic> _mimePartToJson(imap.MimePart part) {
/// Builds a JSON string representing the MIME tree of [msg].
String _buildMimeTreeJson(imap.MimeMessage msg) =>
jsonEncode(_mimePartToJson(msg));
/// Converts a JMAP `bodyStructure` object into the same JSON format used by
/// [_mimePartToJson], so [_parseMimeTree] can deserialise it uniformly.
Map<String, dynamic> _jmapBodyStructureToJson(Map<String, dynamic> m) => {
'contentType': m['type'] as String? ?? 'application/octet-stream',
'filename': m['name'],
'size': m['size'],
'encoding': null,
'children': ((m['subParts'] as List<dynamic>?) ?? [])
.cast<Map<String, dynamic>>()
.map(_jmapBodyStructureToJson)
.toList(),
};