Skip to content

feat(db-repository): replace destroy with destroyAllByServerId and destroyByServerId - #11613

Open
rafaeltonholo wants to merge 5 commits into
thunderbird:mainfrom
rafaeltonholo:rafaeltonholo/create-message-repositories--c-99f9e08
Open

rafaeltonholo wants to merge 5 commits into
thunderbird:mainfrom
rafaeltonholo:rafaeltonholo/create-message-repositories--c-99f9e08

Conversation

@rafaeltonholo

@rafaeltonholo rafaeltonholo commented Sep 23, 2026 •

Copy link
Copy Markdown
Member

Stack

  1. 3da3110 feat(db-repository): add domain models for messages, attachments, and thread messages #11593 (id: c-9653617)
  2. 4c35913 chore(db-repository): convert message download operations to suspend functions #11604 (id: c-e9f0142)
  3. [detached] 18b15b0 feat(core-architecture): add AsyncDataMapper interface for domain-DTO conversion #11606 (id: c-facc931)
  4. 8a393e3 feat(db-repository): add DefaultMessageDataMapper for legacy-to-domain message conversion #11607 (id: c-9ed62a1)
  5. f12481d feat(db-repository): add MessageQueryRepository for querying messages by criteria #11608 (id: c-be36e42)
  6. f0da3d5 feat(db-repository): implement create message in MessageLifecycleRepository #11609 (id: c-639ad93)
  7. 4f9accc feat(db-repository): implement update message in MessageLifecycleRepository #11610 (id: c-a66bc30)
  8. 168347d feat(db-repository): implement move message in MessageLifecycleRepository #11611 (id: c-7f7af18)
  9. 8506a59 feat(db-repository): implement copy and copyAll in MessageLifecycleRepository #11612 (id: c-145c77c)
  10. b0d9b9f feat(db-repository): replace destroy with destroyAllByServerId and destroyByServerId (id: c-99f9e08) <- current

Contribution Summary

Linked Issue/Ticket: Closes #11486
RFC / Technical Design (if applicable):

Description

  • implement message destruction in DefaultMessageLifecycleRepository instead of TODO
  • add MessageQueryRepository.getAllServerIdByFolderId and back K9BackendFolder clearAllMessages/destroyMessages with the new repositories
  • make BackendFolder.destroyMessages and clearAllMessages suspend
  • introduce MessageQueryError.AccountNotFound and UnhandledError, making throwable nullable

AI Disclosure

Select one of the following (mandatory)

  • This contribution does not include any changes created or assisted by AI.
  • This contribution includes changes assisted by AI.
  • This contribution includes changes created by AI.

Contribution Checklist

  • I have read and affirm that my contribution adheres to Mozilla’s Community Participation Guidelines
  • This contribution is in Kotlin where possible
  • This contribution does not use merge commits
  • This contribution adheres to the existing codestyle (run gradlew spotlessCheck to check and gradlew spotlessApply to format your source code; will be checked by CI).
  • This contribution does not break existing unit tests (run gradlew testDebugUnitTest; will be checked by CI).
  • This contribution includes tests for any new functionality, and maintains tests for any updated functionality.
  • This contribution adheres to our Engineering process (RFC/Technical Design/ADR)
  • This PR has a descriptive title and body that accurately outlines all changes made, and contains a reference to any issues that it fixes (e.g. Closes #XXX or Fixes #XXX).

@rafaeltonholo
rafaeltonholo requested a review from a team as a code owner September 23, 2026 12:12
@rafaeltonholo rafaeltonholo added the report: exclude Exclude changes from user-facing reports (internal, minor, or not relevant to users). label Sep 23, 2026
@rafaeltonholo rafaeltonholo added the pr: stacked Must be used on a PR that is stacked on top of other(s) label Sep 23, 2026
@github-actions github-actions Bot added the tb-team Tasks and features handled by project maintainers label Sep 23, 2026
@rafaeltonholo
rafaeltonholo force-pushed the rafaeltonholo/create-message-repositories--c-99f9e08 branch 4 times, most recently from 2625216 to b308012 Compare September 23, 2026 18:34
@wmontwe
wmontwe requested review from wmontwe and removed request for jbott-tbird September 24, 2026 09:32
@wmontwe wmontwe assigned wmontwe and unassigned jbott-tbird Sep 24, 2026
@rafaeltonholo
rafaeltonholo force-pushed the rafaeltonholo/create-message-repositories--c-99f9e08 branch 5 times, most recently from 81352d0 to 302be1d Compare September 24, 2026 15:22
@rafaeltonholo
rafaeltonholo force-pushed the rafaeltonholo/create-message-repositories--c-99f9e08 branch 6 times, most recently from f30e684 to 6235fb8 Compare October 2, 2026 13:02
@rafaeltonholo
rafaeltonholo force-pushed the rafaeltonholo/create-message-repositories--c-99f9e08 branch from 6235fb8 to ffc9344 Compare October 5, 2026 16:17
@rafaeltonholo
rafaeltonholo force-pushed the rafaeltonholo/create-message-repositories--c-99f9e08 branch from ffc9344 to d1e6bdd Compare October 5, 2026 16:26
@thunderbird-botmobile thunderbird-botmobile Bot added the pr-sentinel: ready for review Passed all automated PR Sentinel checks. Ready for maintainer review. label Oct 5, 2026
@rafaeltonholo
rafaeltonholo force-pushed the rafaeltonholo/create-message-repositories--c-99f9e08 branch from d1e6bdd to eafbcb7 Compare October 5, 2026 18:18
@rafaeltonholo
rafaeltonholo force-pushed the rafaeltonholo/create-message-repositories--c-99f9e08 branch from eafbcb7 to 48aba89 Compare October 5, 2026 18:26
@rafaeltonholo
rafaeltonholo force-pushed the rafaeltonholo/create-message-repositories--c-99f9e08 branch from 48aba89 to 5d294a0 Compare October 5, 2026 19:45
@rafaeltonholo
rafaeltonholo force-pushed the rafaeltonholo/create-message-repositories--c-99f9e08 branch from 5d294a0 to 384978b Compare October 5, 2026 19:56
@rafaeltonholo
rafaeltonholo force-pushed the rafaeltonholo/create-message-repositories--c-99f9e08 branch from 384978b to 5aa41e7 Compare October 6, 2026 10:51

@wmontwe wmontwe left a comment •

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This review took some time because these changes touch core application behavior and change it. While refactoring the lifecycle repository to use MessageLifecycleRepository for CRUD operations is the right move, I noticed some issues regarding message content integrity that could lead to data loss.

  1. MessageLifecycleRepository.update() rebuilds the message from the domain model, which loses data. This affects re-downloaded messages and local drafts.
  2. Move to Drafts saves the draft under the source message server UID, which can lose the message.
  3. Each save copies its attachments into cacheDir, but the copies are never used. We also have an attachment store, meaning most attachments are stored twice now. The cache copies stay even when the user deleted the related message and there is no cleanup.
  4. Too verbose logging that prints whole messages
  5. create()/update() throw exceptions where other operations return error outcomes.

logger.debug {
"$LOG_ID creating new message in folder '${folderId ?: "<outbox-id>"}' for account '$accountId'"
}
logger.verbose { "$LOG_ID message = $message" }

@wmontwe wmontwe Oct 12, 2026 •

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why is this writing the whole message to the logger?

Prints:

  • addresses
  • subject
  • plain-text and HTML body
  • headers

Leaks subject, display name and the full plain-text and HTML body.

I think the logger should be used to aid debugging, but not write everything the app does to the logs.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The message object is an object backed by the PII-safe compiler plugin. The addresses are already marked as PII, except for their label.

For the subject, body and headers, we can mark them also as PII.

@rafaeltonholo rafaeltonholo Oct 15, 2026 •

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The MessageHeader is also marked as HasPii, except by the extra property.

logger.debug {
"$LOG_ID updating message '$messageId' in folder '$folderId' for account '$accountId'"
}
logger.verbose { "$LOG_ID message = $message" }

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why is this writing the whole message to the logger?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

val domainFolderId = LegacyFolderIdFactory.of(folderId)
// A message coming from the backend carries no X_DOWNLOADED_* flags yet, so the mapper would
// infer ENVELOPE. The backend tells us explicitly how much was downloaded; that must win.
val domainMessage = mapper.toDomain(message).copy(downloadState = downloadState.toDomainDownloadState())

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

mapper.toDomain(message) runs AttachmentResolver.resolveAttachmentFile on every attachment, which copies the decoded content into cacheDir. The message store already keeps the same attachment encoded (in the attachment directory above 16 KB, in the database below). So every synced, sent or saved-draft message now stores its attachments twice and the cache copy isn't even used: create() saves the original message through LegacyMessageSource.

Nothing deletes these copies. deleteStaleVersions only runs for a LocalPart, and deleting or moving a message doesn't touch them. The decoded content can also stay on disk after the user deletes the message.

The same happens in sendMessageCompat and DraftOperations.toDomainMessage.

This also adds extra IO to every message save.

val messageData = message.toSaveMessageData(plaintextSubject)

return messageStore.saveLocalMessage(folderId, messageData, existingDraftId)
val domainMessage = message.toDomainMessage(plaintextSubject, account, folderId)

@wmontwe wmontwe Oct 13, 2026 •

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

toDomainMessage writes the plaintext subject into envelope.subject, and then update() → toDto writes it to the stored Subject: header. For an encrypted draft, that replaces ... with the real subject, which undoes subject protection. Before this change, the plaintext subject only went into the display-subject column. Please pass it separately rather than through the envelope.

Comment on lines +198 to +214
private suspend fun LegacyMessage.toDomainMessage(
subject: String?,
account: LegacyAccountDto,
folderId: Long,
): Message {
setFlag(Flag.X_DOWNLOADED_FULL, true)
accountId = account.id
val domainMessage = messageDataMapper.toDomain(this)
val envelop = if (subject.isNullOrBlank()) {
domainMessage.envelope
} else {
domainMessage.envelope.copy(subject = subject)
}
return domainMessage.copy(
folderId = LegacyFolderIdFactory.of(folderId),
envelope = envelop,
)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

moveToDraftsFolderInBackground (MessagingController.java:1995) passes in a LocalMessage that still has its server UID from the source folder. The mapper turns that UID into serverId, so create() calls saveRemoteMessage(draftsFolder, <source UID>). Before this change, saveLocalMessage always generated a local placeholder UID.

What follows:

  • processPendingAppend skips the upload because the UID has no local prefix
    (MessagingController.java:875).
  • The next Drafts sync deletes the row, because that UID isn't on the server. If the UID happens to match a real draft, the row gets overwritten, or create fails with MessageAlreadyExists.
  • The source message has already been destroyed by then, so the content is lost.

Maybe clearing serverId and id here so drafts are always created as local messages.

Comment thread legacy/core/src/main/java/com/fsck/k9/controller/DraftOperations.kt
…sitory

- Wire MessageLifecycleRepository into MessagingController for sending messages
- Replace direct MessageStore calls with repository.create() in sendMessage()
- Add DefaultMessageLifecycleRepository implementing create() with legacy adapter logic
- Add messageLifecycleRepository, messageDataMapper, and messageIdLegacyEntityIdFactory dependencies
- Convert domain Message to legacy DTO, save to outbox folder, and initialize outbox state
- Add MessageLifecycleRepository registration in MailMessageModule
- Add feature.mail.message.api dependency to legacy core

GG-ID: c-639ad93
…sitory

- Add update(message, accountId, folderId) to MessageLifecycleRepository with legacy adapter
- Add MessageAlreadyExists error and duplicate detection in create() via MessageQueryRepository
- Use create/update in K9BackendFolder.saveMessage, recovering from MessageAlreadyExists
- Use create/update in DraftOperations.saveDraftLocally
- Derive legacy download state from message flags in the mapper
- Add update and duplicate-detection tests to DefaultMessageLifecycleRepositoryTest

GG-ID: c-a66bc30

GG-Parent: c-639ad93
…tory

- Add move(messageId, destinationFolderId, accountId) and moveAll() to MessageLifecycleRepository
- Implement both in DefaultMessageLifecycleRepository with legacy adapter, mapping store exceptions to UnhandledError
- Replace MessageStore.moveMessage/moveMessages calls in MessagingController with repository move/moveAll
- Add move/moveAll tests to DefaultMessageLifecycleRepositoryTest and sent-message move tests to MessagingControllerTest

GG-ID: c-7f7af18

GG-Parent: c-a66bc30
…pository

- Add accountId parameter to MessageLifecycleRepository.copy
- Add copyAll for copying multiple messages to a destination folder
- Implement both operations in DefaultMessageLifecycleRepository via legacy MessageStore
- Replace direct messageStore.copyMessages call in MessagingController with repository-backed copyAll
- Extract convertLegacyMessageIdsToDomainMessageIds and convertDomainIdMappingToLegacyIdMapping helpers
- Add tests for copy and copyAll success, store failure, and unknown account cases

GG-ID: c-145c77c

GG-Parent: c-7f7af18
…stroyByServerId

- implement message destruction in DefaultMessageLifecycleRepository instead of TODO
- add MessageQueryRepository.getAllServerIdByFolderId and back K9BackendFolder clearAllMessages/destroyMessages with the new repositories
- make BackendFolder.destroyMessages and clearAllMessages suspend
- introduce MessageQueryError.AccountNotFound and UnhandledError, making throwable nullable

GG-ID: c-99f9e08

GG-Parent: c-145c77c
@rafaeltonholo
rafaeltonholo force-pushed the rafaeltonholo/create-message-repositories--c-99f9e08 branch from 5aa41e7 to 1475601 Compare October 15, 2026 18:31

This branch was successfully deployed

2 active (1 outdated) deployments
botmobile — 14756016 Deployed Oct 15, 2026 by rafaeltonholo via PR Sentinel #203
review — b0d9b9f0 Deployed Sep 23, 2026 by rafaeltonholo via label-tb-team #541
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

pr: stacked Must be used on a PR that is stacked on top of other(s) pr-sentinel: ready for review Passed all automated PR Sentinel checks. Ready for maintainer review. report: exclude Exclude changes from user-facing reports (internal, minor, or not relevant to users). tb-team Tasks and features handled by project maintainers

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Global Database 0.1.8 — Repository pattern — Create repositories for Message related data access

3 participants