Fix email ID collision: include mailboxPath in local ID #502
Closed
opened 2026-06-06 18:15:33 +00:00 by guettlibot
·
1 comment
No Branch/Tag Specified
main
issue-563-agentloop-validation
dummy-pr-test
issue-560-fix-firebase-run-url
issue-539-stable-imap-uid
issue-533-shared-email-list
plan-issue-555
drop-nix
plan-issue-484
plan-issue-539
plan-issue-535
plan-issue-474
plan-issue-533
fix-dagger-engineless-precommit
issue-521-fix-deploy-yml-wait-time-api
issue-502-fix-email-id-collision-mailbox
issue-492-eliminate-duplicate-build-runner
issue-494-website-change-detection
issue-491-parallelize-check
issue-478-fix-stalwart-dual-stack-bind
issue-475-allowed-addresses-glob
issue-473-search-result-reorder
issue-453-update-agentloop-defaults
issue-466-structured-search
issue-505-exclude-chaos-monkey-from-regular-ci
issue-509-fix-search-result-sorting
fix-ink-sparkle-remaining-tests
issue-506-fix-search-emails-tests
issue-504-runner-wait-time
issue-488-search-notes
issue-472-changelog-issue-links
issue-501-folder-search-local-sqlite
issue-486-fix-stale-test-shader-mismatch
fix/prevent-settled-search-rerun-473
issue-467-fix-search-stale-results
issue-446-installed-versions-in-changelog
issue-462-fix-pr
issue-448-chaos-monkey-test
issue-436-notes-on-emails
issue-429-unify-mail-display
issue-422-move-to-folder-create-new
issue-414-ensure-not-run-as-root
issue-424-unify-email-list-views
issue-419-trusted-senders-page
issue-425-fix-prs
test-foo
issue-421-bug-report
issue-383-fix-ci
issue-394-fix-deploy-flutter-version
issue-391-fix-ci-double-trigger
issue-376-combined-inbox-v2
issue-376-combined-inbox
issue-384-fix-open-prs
sops-migrate
issue-339-safe-first-on-imap-fetch
issue-340-try-catch-measure-height
issue-342-pin-intl-version
issue-341-guard-threademails-last
issue-335-agentloop-code-test
issue-329-fix
issue-315-fix
issue-320-fix
issue-325-fix
issue-312-fix
issue-311-fix
issue-305-fix
issue-304-fix
issue-299-fix
issue-300-fix
issue-298-fix
issue-296-fix
issue-294-fix
issue-289-fix
issue-288-fix
issue-287-fix
issue-286-fix
issue-277-fix
issue-282-fix
issue-280-fix
issue-272-fix
issue-268-fix
issue-267-fix
issue-266-fix
issue-258-fix
issue-260-fix
issue-257-fix
issue-253-fix
issue-216-fix
issue-251-fix
issue-249-fix
issue-question-fixes
issue-235-fix
issue-236-fix-v2
issue-237-fix
issue-236-fix
issue-228-fix
issue-217-fix
issue-214-fix
issue-213-fix
issue-208-fix
issue-205-fix
issue-204-fix
issue-203-fix
issue-202-fix
issue-129-fix
issue-161-fix
issue-160-fix
issue-201-fix
issue-210-fix
issue-198-fix
issue-200-fix
issue-144-fix
issue-199-fix
fix/playstore-upload-use-requests
issue-193-fix
issue-186-fix
issue-185-fix
issue-192-fix
issue-183-fix
issue-175-fix
issue-172-fix
issue-171-fix
issue-167-fix
issue-136-fix
issue-162-fix
issue-179-fix
issue-155-fix
issue-154-fix
issue-152-fix
issue-151-fix
issue-141-fix
issue-150-fix
issue-164-fix
migrate-to-dagger
task/d1-ci-matrix
task/a4-typeconverter-json
task/u7-onboarding-walkthrough
task/d3-sync-doc
task/a5-layer-boundary-lint
task/t5-golden-tests
task/p5-date-cache
task/s4-link-handling
task/p3-html-parse-isolate
task/u8-mark-all-read
task/u3-recent-searches
task/a3-jmap-injectable-http-client
task/r5-tls-error-handling
fix/playstore-redirect-retry
task/t3-repository-contract-tests
task/p2-email-list-pagination
task/p1-fts5-search
fix/playstore-upload-timeout
task/a1-email-detail-notifier
fix/upgrade-workmanager-0.9
fix/android-core-library-desugaring
task/p4-db-indexes
task/r3-html-error-boundary
task/d2-check-coverage
task/a2-email-tile
task/t4-migration-tests
task/t2-widget-tests
task/t1-email-repo-coverage
task/u6-connection-status
task/u4-push-notifications
task/u2-draft-sync
task/u1-list-unsubscribe
task/s2-hostname-validation
task/r6-reliability-fuzz-tests
task/r4-sync-error-banner
task/r2-force-resync
task/r1-undo-history-persistence
No results found.
Labels
Clear labels
NeedSupervisor
State/InProgress
State/Later
State/Planned
automerge
ci-failure
do-not-merge
loop/code
loop/code-ci-pending
loop/code-done
loop/code-in-process
loop/merge
loop/merge-done
loop/merge-in-process
loop/plan
loop/plan-done
loop/plan-in-process
Issue escalated to a human supervisor; agentloop will skip it until cleared.
Eligible for automatic merge by CI
Issue opened by agentloop to track a failing CI workflow; used for deduplication.
Plan PR — review only, do not merge.
Add to run the built-in "code" prompt; override at prompts/code.md.
Prompt "code" finished; waiting for the PR's CI to pass before advancing.
Prompt "code" finished successfully.
Agent for the "code" prompt is currently running on this issue.
Managed by agentloop
Managed by agentloop
Managed by agentloop
Add to run the built-in "plan" prompt; override at prompts/plan.md.
Prompt "plan" finished successfully.
Agent for the "plan" prompt is currently running on this issue.
No labels
loop/merge
Milestone
No items
No Milestone
Projects
Clear projects
No projects
No Assignees
Notifications
Due Date
No due date set.
Dependencies
No dependencies set.
Reference: guettli/sharedinbox#502
Reference in New Issue
Block a user
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.
Problem
The local email ID is constructed as
'$accountId:$uid'(sync: line 618, search: line 3085 ofemail_repository_impl.dart). IMAP UIDs are mailbox-scoped — UID 50 in INBOX and UID 50 in Archive are different emails, yet both get the same local ID.insertOnConflictUpdatesilently overwrites whichever was stored first, corrupting the local DB. A force-full-sync cannot fix this because it repeats the same collision.Plan
Change the ID scheme to
'$accountId:$mailboxPath:$uid'everywhere an ID is constructed from an IMAP UID:_fetchAndUpsertImap(line 618)_refreshFlagsImap(line 563)searchEmails(line 3085) — moot if #501 (offline search) is implemented first; remove this call entirely.accountId:uid— grep for'$accountId:$'pattern.Write a schema migration (Drift
MigrationStrategy):emailstable rows whoseiddoes not contain a mailbox segment.(account_id, mailbox_path, uid)columns already stored on each row.email_bodies.email_idforeign key to match.Update all code that constructs or parses an email ID by raw string manipulation (e.g.
emailId.substring(emailId.indexOf(':') + 1)in the JMAP body path, line 333–334).Add a unit test that inserts two emails with the same UID in different mailboxes and asserts both are retrievable independently.
Verify that after the migration existing data is intact and
getEmailreturns the correct record for each mailbox.Note
If issue #501 (offline-first search) is implemented first, step 1 for
searchEmailsis already covered — the IMAP live search that constructed IDs from server UIDs will be gone. The remaining ID fix is still needed for sync.PR created: https://codeberg.org/guettli/sharedinbox/pulls/511
Here's a summary of what was done to fix issue #502:
Root cause: IMAP UIDs are mailbox-scoped, but the local email ID was
accountId:uid. This meant UID 50 in INBOX and UID 50 in Archive generated the same ID, causinginsertOnConflictUpdateto silently corrupt whichever email was stored first.Changes made:
lib/data/repositories/email_repository_impl.dart(lines 563, 618): Changed email ID construction from'${account.id}:$uid'to'${account.id}:$mailboxPath:$uid'in both_refreshFlagsImapand_fetchAndUpsertImap.lib/core/db_schema_version.dart: Bumped schema version 40 → 41.lib/data/db/database.dart: Added migration v41 that:PRAGMA defer_foreign_keys = ONto safely update bothemail_bodies.email_id(FK) andemails.id(PK) within the same transactionemails.thread_idwhere it was set to the email's own ID (fallback for messages without a Message-ID header)test/unit/migration_test.dart: Added av40→v41migration test that verifies IDs, email_bodies, thread_ids, and threads are all correctly updated.test/unit/email_repository_impl_test.dart: Added a unit test asserting that two emails with the same UID in different mailboxes get distinct IDs and are independently retrievable.All 338 unit tests pass.