Notes on mails #436

Closed
opened 2026-06-05 05:32:52 +00:00 by guettli · 2 comments
guettli commented 2026-06-05 05:32:52 +00:00 (Migrated from codeberg.org)

It should be possible to add notes to mails.

N notes, which can be added/deleted individually.

How to store the data? Data must be on the related IMAP/Jmap server.

Challenge: when moving a message from folder A to B, then the note must not get lost. Afaik IMAP IDs get lost during move.

Local DB fiddling must not be used. One account could be used by several people.

It should be possible to add notes to mails. N notes, which can be added/deleted individually. How to store the data? Data must be on the related IMAP/Jmap server. Challenge: when moving a message from folder A to B, then the note must not get lost. Afaik IMAP IDs get lost during move. Local DB fiddling must not be used. One account could be used by several people.
guettlibot commented 2026-06-05 05:40:04 +00:00 (Migrated from codeberg.org)

Now I have enough context. Let me write the implementation plan.


Implementation Plan: Notes on Emails (#436)

Design: Notes folder on the IMAP/JMAP server

Store each note as a message in a dedicated folder (e.g. SharedInbox/Notes) inside the user's mail account. This satisfies every constraint:

  • On the server — no local-only data
  • Multi-user safe — every client connected to the same account reads the same folder
  • Move-stable — notes are keyed by the Message-ID header (RFC 2822), which is immutable across folder moves, not by the IMAP UID

Each note message carries two custom headers:

  • X-SharedInbox-Note-For: <original-message-id> — links the note to the source email
  • X-SharedInbox-Note-Id: <uuid> — stable per-note identifier (survives UID reassignment in the Notes folder itself)

Steps

1. Schema v39 — Drift EmailNotes table

Add to lib/data/db/database.dart:

class EmailNotes extends Table {
  TextColumn get id        => text()();  // uuid (X-SharedInbox-Note-Id)
  TextColumn get accountId => text().references(Accounts, #id, onDelete: KeyAction.cascade)();
  TextColumn get messageId => text()();  // X-SharedInbox-Note-For value
  TextColumn get noteText  => text()();
  IntColumn  get serverUid => integer()(); // UID in Notes folder (needed for IMAP delete)
  DateTimeColumn get createdAt => dateTime()();

  @override
  Set<Column> get primaryKey => {id};
}

Register it in @DriftDatabase(tables: [...]) and add the if (from < 39) migration block. Run dart run build_runner build --delete-conflicting-outputs (alias: task codegen).


2. Repository interface — lib/core/repositories/note_repository.dart

abstract class NoteRepository {
  Stream<List<EmailNote>> observeNotes(String accountId, String messageId);
  Future<void> syncNotes(String accountId, String messageId);
  Future<void> addNote(String accountId, String messageId, String text);
  Future<void> deleteNote(String accountId, String noteId);
}

3. IMAP backend

  • addNote: APPEND SharedInbox/Notes with a minimal RFC 2822 message whose body is the note text and whose headers include X-SharedInbox-Note-For and X-SharedInbox-Note-Id. Create the folder first (CREATE + SUBSCRIBE) if the server returns a NO response.
  • syncNotes: SEARCH HEADER X-SharedInbox-Note-For <message-id>, then FETCH <uid-set> (UID RFC822.SIZE BODY[HEADER.FIELDS (X-SharedInbox-Note-Id X-SharedInbox-Note-For DATE)] BODY[TEXT]). Upsert results into the local EmailNotes table.
  • deleteNote: SEARCH HEADER X-SharedInbox-Note-Id <uuid> to get the UID, then STORE +FLAGS.SILENT \Deleted + EXPUNGE.

4. JMAP backend

  • addNote: Email/setcreate in the Notes mailbox (look up or create mailbox by name). Pass custom headers as headers array entries.
  • syncNotes: Email/query with a header:X-SharedInbox-Note-For:as:Text:equals filter, followed by Email/get for matched IDs.
  • deleteNote: Email/query filtering on X-SharedInbox-Note-Id, then Email/setdestroy.

5. Repository implementation — lib/data/repositories/note_repository_impl.dart

observeNotes queries the local Drift table (returns a stream, no network call). addNote and deleteNote update the server first, then update the local cache on success. syncNotes is called explicitly (on-demand from the UI).


6. DI — lib/di.dart

Register a noteRepositoryProvider (similar to emailRepositoryProvider), providing NoteRepositoryImpl which receives the AppDatabase, ImapClientFactory, and JmapClient it needs.


7. UI — lib/ui/screens/email_detail_screen.dart

  • Add a Notes section below the email body (an ExpansionTile or always-visible card list).
  • On expand / screen open: call syncNotes once for the current email's messageId; watch observeNotes for live updates.
  • Each note: a ListTile with the note text, a formatted createdAt timestamp, and a trailing IconButton(icon: Icon(Icons.delete)).
  • An "Add note" TextButton or FAB opens a small AlertDialog with a multi-line TextField + confirm button.
  • If email.messageId == null (rare — non-compliant senders): disable the add-note button and show a tooltip explaining the note cannot be attached without a stable Message-ID.

Out of scope for v1

  • Editing existing notes (add + delete satisfies the requirement)
  • Background sync of the Notes folder during regular account sync
  • Searching by note content
  • Configurable Notes folder name (hardcode SharedInbox/Notes)
Now I have enough context. Let me write the implementation plan. --- ## Implementation Plan: Notes on Emails (#436) ### Design: Notes folder on the IMAP/JMAP server Store each note as a message in a dedicated folder (e.g. `SharedInbox/Notes`) inside the user's mail account. This satisfies every constraint: - **On the server** — no local-only data - **Multi-user safe** — every client connected to the same account reads the same folder - **Move-stable** — notes are keyed by the `Message-ID` header (RFC 2822), which is immutable across folder moves, not by the IMAP UID Each note message carries two custom headers: - `X-SharedInbox-Note-For: <original-message-id>` — links the note to the source email - `X-SharedInbox-Note-Id: <uuid>` — stable per-note identifier (survives UID reassignment in the Notes folder itself) --- ### Steps **1. Schema v39 — Drift `EmailNotes` table** Add to `lib/data/db/database.dart`: ```dart class EmailNotes extends Table { TextColumn get id => text()(); // uuid (X-SharedInbox-Note-Id) TextColumn get accountId => text().references(Accounts, #id, onDelete: KeyAction.cascade)(); TextColumn get messageId => text()(); // X-SharedInbox-Note-For value TextColumn get noteText => text()(); IntColumn get serverUid => integer()(); // UID in Notes folder (needed for IMAP delete) DateTimeColumn get createdAt => dateTime()(); @override Set<Column> get primaryKey => {id}; } ``` Register it in `@DriftDatabase(tables: [...])` and add the `if (from < 39)` migration block. Run `dart run build_runner build --delete-conflicting-outputs` (alias: `task codegen`). --- **2. Repository interface — `lib/core/repositories/note_repository.dart`** ```dart abstract class NoteRepository { Stream<List<EmailNote>> observeNotes(String accountId, String messageId); Future<void> syncNotes(String accountId, String messageId); Future<void> addNote(String accountId, String messageId, String text); Future<void> deleteNote(String accountId, String noteId); } ``` --- **3. IMAP backend** - **`addNote`**: `APPEND SharedInbox/Notes` with a minimal RFC 2822 message whose body is the note text and whose headers include `X-SharedInbox-Note-For` and `X-SharedInbox-Note-Id`. Create the folder first (`CREATE` + `SUBSCRIBE`) if the server returns a NO response. - **`syncNotes`**: `SEARCH HEADER X-SharedInbox-Note-For <message-id>`, then `FETCH <uid-set> (UID RFC822.SIZE BODY[HEADER.FIELDS (X-SharedInbox-Note-Id X-SharedInbox-Note-For DATE)] BODY[TEXT])`. Upsert results into the local `EmailNotes` table. - **`deleteNote`**: `SEARCH HEADER X-SharedInbox-Note-Id <uuid>` to get the UID, then `STORE +FLAGS.SILENT \Deleted` + `EXPUNGE`. --- **4. JMAP backend** - **`addNote`**: `Email/set` → `create` in the Notes mailbox (look up or create mailbox by name). Pass custom headers as `headers` array entries. - **`syncNotes`**: `Email/query` with a `header:X-SharedInbox-Note-For:as:Text:equals` filter, followed by `Email/get` for matched IDs. - **`deleteNote`**: `Email/query` filtering on `X-SharedInbox-Note-Id`, then `Email/set` → `destroy`. --- **5. Repository implementation — `lib/data/repositories/note_repository_impl.dart`** `observeNotes` queries the local Drift table (returns a stream, no network call). `addNote` and `deleteNote` update the server first, then update the local cache on success. `syncNotes` is called explicitly (on-demand from the UI). --- **6. DI — `lib/di.dart`** Register a `noteRepositoryProvider` (similar to `emailRepositoryProvider`), providing `NoteRepositoryImpl` which receives the `AppDatabase`, `ImapClientFactory`, and `JmapClient` it needs. --- **7. UI — `lib/ui/screens/email_detail_screen.dart`** - Add a **Notes section** below the email body (an `ExpansionTile` or always-visible card list). - On expand / screen open: call `syncNotes` once for the current email's `messageId`; watch `observeNotes` for live updates. - Each note: a `ListTile` with the note text, a formatted `createdAt` timestamp, and a trailing `IconButton(icon: Icon(Icons.delete))`. - An **"Add note"** `TextButton` or FAB opens a small `AlertDialog` with a multi-line `TextField` + confirm button. - If `email.messageId == null` (rare — non-compliant senders): disable the add-note button and show a tooltip explaining the note cannot be attached without a stable Message-ID. --- ### Out of scope for v1 - Editing existing notes (add + delete satisfies the requirement) - Background sync of the Notes folder during regular account sync - Searching by note content - Configurable Notes folder name (hardcode `SharedInbox/Notes`)
guettlibot commented 2026-06-05 15:05:04 +00:00 (Migrated from codeberg.org)

agentloop: agent exceeded its 30m0s runtime budget and was stopped.

agent stderr tail:

[acpx] session agentloop-sharedinbox-issue-436 (cf8c79b3-a222-4ac9-b3ac-634d8e8015fd) · /home/si/agentloop/loop-data/sharedinbox/issues/436 · agent needs reconnect
agentloop: agent exceeded its 30m0s runtime budget and was stopped. agent stderr tail: ``` [acpx] session agentloop-sharedinbox-issue-436 (cf8c79b3-a222-4ac9-b3ac-634d8e8015fd) · /home/si/agentloop/loop-data/sharedinbox/issues/436 · agent needs reconnect ```
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: guettli/sharedinbox#436