@@ -4,6 +4,7 @@ import app.k9mail.legacy.mailstore.ListenableMessageStore
44import app.k9mail.legacy.mailstore.MessageStoreManager
55import app.k9mail.legacy.mailstore.SaveMessageData
66import com.fsck.k9.mailstore.SaveMessageDataCreator
7+ import kotlin.coroutines.cancellation.CancellationException
78import kotlinx.coroutines.CoroutineDispatcher
89import kotlinx.coroutines.Dispatchers
910import kotlinx.coroutines.withContext
@@ -55,7 +56,11 @@ class DefaultMessageLifecycleRepository(
5556 " $LOG_ID creating new message in folder '${folderId ? : " <outbox-id>" } ' for account '$accountId '"
5657 }
5758 logger.verbose { " $LOG_ID message = $message " }
58- val messageData = message.toSaveMessageData()
59+ val messageData = message.toSaveMessageData(
60+ logger = logger,
61+ saveMessageDataCreator = saveMessageDataCreator,
62+ messageDataMapper = messageMapper,
63+ )
5964 val messageStore = messageStoreManager.getMessageStore(accountId)
6065 val legacyFolderId = if (folderId == null ) {
6166 outboxFolderManager.getOutboxFolderId(accountId)
@@ -107,8 +112,61 @@ class DefaultMessageLifecycleRepository(
107112 override suspend fun move (
108113 messageId : MessageId ,
109114 destinationFolderId : FolderId ,
110- ): Outcome <MessageId , MessageLifecycleError > {
111- TODO (" Not yet implemented" )
115+ accountId : AccountId ,
116+ ): Outcome <MessageId , MessageLifecycleError > = withContext(ioDispatcher) {
117+ logger.debug { " $LOG_ID moving '$messageId ' to folder '$destinationFolderId ' for account '$accountId '" }
118+ val legacyMessageId = messageIdLegacyEntityIdFactory.toLegacyId(messageId)
119+ val legacyFolderId = folderIdLegacyEntityIdFactory.toLegacyId(destinationFolderId)
120+ logger.verbose {
121+ " $LOG_ID message id '$messageId ' -> legacy id '$legacyMessageId ', " +
122+ " folder id '$destinationFolderId ' -> legacy id '$legacyFolderId '"
123+ }
124+
125+ runLegacy(operation = " move message '$messageId '" ) {
126+ val messageStore = messageStoreManager.getMessageStore(accountId)
127+ val legacyDestinationMessageId = messageStore.moveMessage(legacyMessageId, legacyFolderId)
128+ messageIdLegacyEntityIdFactory.of(legacyDestinationMessageId).also { destinationMessageId ->
129+ logger.verbose { " $LOG_ID moved message '$messageId ' -> '$destinationMessageId '" }
130+ }
131+ }
132+ }
133+
134+ override suspend fun moveAll (
135+ messageIds : List <MessageId >,
136+ destinationFolderId : FolderId ,
137+ accountId : AccountId ,
138+ ): Outcome <Map <MessageId , MessageId >, MessageLifecycleError> = withContext(ioDispatcher) {
139+ if (messageIds.isEmpty()) {
140+ logger.verbose { " $LOG_ID nothing to move to folder '$destinationFolderId ' for account '$accountId '" }
141+ return @withContext Outcome .success(emptyMap())
142+ }
143+ logger.debug {
144+ " $LOG_ID moving ${messageIds.size} messages to folder '$destinationFolderId ' for account '$accountId '"
145+ }
146+ val legacyMessageIds = messageIds.map(messageIdLegacyEntityIdFactory::toLegacyId)
147+ val legacyFolderId = folderIdLegacyEntityIdFactory.toLegacyId(destinationFolderId)
148+ logger.verbose {
149+ " $LOG_ID message ids $messageIds -> legacy ids $legacyMessageIds , " +
150+ " folder id '$destinationFolderId ' -> legacy id '$legacyFolderId '"
151+ }
152+
153+ runLegacy(operation = " move ${messageIds.size} messages" ) {
154+ val messageStore = messageStoreManager.getMessageStore(accountId)
155+ messageStore.moveMessages(legacyMessageIds, legacyFolderId)
156+ .entries
157+ .associate { (sourceLegacyId, destinationLegacyId) ->
158+ messageIdLegacyEntityIdFactory.of(sourceLegacyId) to
159+ messageIdLegacyEntityIdFactory.of(destinationLegacyId)
160+ }
161+ .also { mapping ->
162+ logger.verbose {
163+ val lines = mapping.entries.joinToString(" \n " ) { (source, destination) ->
164+ " '$source ' -> '$destination '"
165+ }
166+ " $LOG_ID moved messages to folder '$destinationFolderId ':\n ${lines.prependIndent(" " )} "
167+ }
168+ }
169+ }
112170 }
113171
114172 override suspend fun copy (
@@ -126,6 +184,23 @@ class DefaultMessageLifecycleRepository(
126184 TODO (" Not yet implemented" )
127185 }
128186
187+ /* *
188+ * Runs a legacy store operation and converts any thrown exception into a [MessageLifecycleError.UnhandledError].
189+ * [CancellationException] is rethrown so structured concurrency keeps working.
190+ */
191+ @Suppress(" TooGenericExceptionCaught" )
192+ private inline fun <T > runLegacy (
193+ operation : String ,
194+ block : () -> T ,
195+ ): Outcome <T , MessageLifecycleError > = try {
196+ Outcome .success(block())
197+ } catch (e: CancellationException ) {
198+ throw e
199+ } catch (e: Exception ) {
200+ logger.error(throwable = e) { " $LOG_ID $operation failed" }
201+ Outcome .failure(MessageLifecycleError .UnhandledError (e))
202+ }
203+
129204 private suspend fun verifyMessageExists (
130205 folderId : FolderId ? ,
131206 messageServerId : MessageServerId ? ,
@@ -169,31 +244,35 @@ class DefaultMessageLifecycleRepository(
169244 )
170245 return messageIdLegacyEntityIdFactory.of(legacyMessageId)
171246 }
247+ }
172248
173- private fun MessageDownloadState.toLegacyDownloadState (): LegacyMessageDownloadState = when (this ) {
174- MessageDownloadState .ENVELOPE -> LegacyMessageDownloadState .ENVELOPE
175- MessageDownloadState .PARTIAL -> LegacyMessageDownloadState .PARTIAL
176- MessageDownloadState .FULL -> LegacyMessageDownloadState .FULL
177- }
249+ private fun MessageDownloadState.toLegacyDownloadState (): LegacyMessageDownloadState = when (this ) {
250+ MessageDownloadState .ENVELOPE -> LegacyMessageDownloadState .ENVELOPE
251+ MessageDownloadState .PARTIAL -> LegacyMessageDownloadState .PARTIAL
252+ MessageDownloadState .FULL -> LegacyMessageDownloadState .FULL
253+ }
178254
179- private suspend fun Message.toSaveMessageData (): SaveMessageData {
180- val downloadState = downloadState.toLegacyDownloadState()
181- return when (val source = source) {
182- is LegacyMessageSource -> {
183- logger.verbose {
184- " $LOG_ID convert message -> save message data using message source ($source )"
185- }
186- saveMessageDataCreator.createSaveMessageData(
187- source.message,
188- downloadState,
189- envelope.subject,
190- )
255+ private suspend fun Message.toSaveMessageData (
256+ logger : Logger ,
257+ saveMessageDataCreator : SaveMessageDataCreator ,
258+ messageDataMapper : MessageDataMapper <LegacyMessageDto >,
259+ ): SaveMessageData {
260+ val downloadState = downloadState.toLegacyDownloadState()
261+ return when (val source = source) {
262+ is LegacyMessageSource -> {
263+ logger.verbose {
264+ " $LOG_ID convert message -> save message data using message source ($source )"
191265 }
266+ saveMessageDataCreator.createSaveMessageData(
267+ source.message,
268+ downloadState,
269+ envelope.subject,
270+ )
271+ }
192272
193- else -> {
194- logger.verbose { " $LOG_ID convert message -> save message data using message mapper" }
195- saveMessageDataCreator.createSaveMessageData(messageMapper.toDto(this ), downloadState)
196- }
273+ else -> {
274+ logger.verbose { " $LOG_ID convert message -> save message data using message mapper" }
275+ saveMessageDataCreator.createSaveMessageData(messageDataMapper.toDto(this ), downloadState)
197276 }
198277 }
199278}
0 commit comments