fix(snooze): create Snoozed folder automatically on first use (#75)

Two bugs prevented snoozing in a brand-new IMAP/JMAP account:

- IMAP flush read `payload['mailboxPath']` which doesn't exist in snooze
  payloads (they use 'src'); selecting the wrong (null) mailbox caused the
  operation to fail.  Now uses `payload['mailboxPath'] ?? payload['src']`.

- JMAP flush had no path to create the Snoozed mailbox when the folder
  didn't already exist on the server.  Flush now calls `Mailbox/set` to
  create it whenever `dest == 'Snoozed'` (the sentinel used when the folder
  was absent at enqueue time), then substitutes the real JMAP mailbox ID.

Tests added for both code paths using a spy IMAP client and a mock JMAP
HTTP client respectively.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Thomas SharedInbox
2026-05-15 17:35:36 +02:00
co-authored by Claude Sonnet 4.6
parent 99df6f5fd0
commit c649ee3414
3 changed files with 240 additions and 3 deletions
@@ -2047,7 +2047,8 @@ class EmailRepositoryImpl implements EmailRepository {
) async {
final payload = jsonDecode(row.payload) as Map<String, dynamic>;
final uid = payload['uid'] as int;
final mailboxPath = payload['mailboxPath'] as String;
// snooze/unsnooze payloads use 'src' for the source folder; all others use 'mailboxPath'.
final mailboxPath = (payload['mailboxPath'] ?? payload['src']) as String;
final seq = imap.MessageSequence.fromId(uid, isUid: true);
await client.selectMailboxByPath(mailboxPath);
@@ -2186,8 +2187,29 @@ class EmailRepositoryImpl implements EmailRepository {
final until = payload['until'] as String;
final timestamp = until.replaceAll(':', '').replaceAll('-', '');
final keyword = 'snz:$timestamp';
final destMailboxId = payload['dest'] as String;
var destMailboxId = payload['dest'] as String;
final srcMailboxId = payload['src'] as String;
// When the Snoozed folder didn't exist at enqueue time, 'dest' holds
// the literal name 'Snoozed' rather than a JMAP mailbox ID. Create it.
if (destMailboxId == 'Snoozed') {
final createResps = await jmap.call([
[
'Mailbox/set',
{
'accountId': jmap.accountId,
'create': {
'new-snoozed': {'name': 'Snoozed', 'role': 'snoozed'},
},
},
'0',
],
]);
final createResult = _responseArgs(createResps, 0, 'Mailbox/set');
final created = createResult['created'] as Map<String, dynamic>?;
final newId = (created?['new-snoozed']
as Map<String, dynamic>?)?['id'] as String?;
if (newId != null) destMailboxId = newId;
}
responses = await jmap.call([
[
'Email/set',