Skip to content

Commit 3ffbfab

Browse files
committed
feat(message-export): add eml to MessageViewFragment
1 parent cb4fcda commit 3ffbfab

File tree

2 files changed

+66
-2
lines changed

2 files changed

+66
-2
lines changed

legacy/ui/legacy/build.gradle.kts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ dependencies {
3434
implementation(projects.feature.settings.import)
3535
implementation(projects.feature.telemetry.api)
3636
implementation(projects.feature.mail.message.list)
37+
implementation(projects.feature.mail.message.export.api)
3738

3839
compileOnly(projects.mail.protocols.imap)
3940

@@ -69,6 +70,8 @@ dependencies {
6970
implementation(libs.mime4j.core)
7071
implementation(libs.kotlinx.coroutines.core)
7172
implementation(libs.kotlinx.coroutines.android)
73+
implementation(libs.kotlinx.datetime)
74+
implementation(libs.uri)
7275

7376
implementation(libs.glide)
7477
annotationProcessor(libs.glide.compiler)

legacy/ui/legacy/src/main/java/com/fsck/k9/ui/messageview/MessageViewFragment.kt

Lines changed: 63 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,9 +26,11 @@ import androidx.core.view.WindowInsetsCompat.Type.navigationBars
2626
import androidx.fragment.app.DialogFragment
2727
import androidx.fragment.app.Fragment
2828
import androidx.fragment.app.setFragmentResultListener
29+
import androidx.lifecycle.lifecycleScope
2930
import app.k9mail.core.android.common.activity.CreateDocumentResultContract
3031
import app.k9mail.core.ui.legacy.designsystem.atom.icon.Icons
3132
import app.k9mail.legacy.message.controller.MessageReference
33+
import com.eygraber.uri.toKmpUri
3234
import com.fsck.k9.K9
3335
import com.fsck.k9.activity.MessageCompose
3436
import com.fsck.k9.activity.MessageLoaderHelper
@@ -45,6 +47,7 @@ import com.fsck.k9.mail.Flag
4547
import com.fsck.k9.mailstore.AttachmentViewInfo
4648
import com.fsck.k9.mailstore.LocalMessage
4749
import com.fsck.k9.mailstore.MessageViewInfo
50+
import com.fsck.k9.provider.RawMessageProvider
4851
import com.fsck.k9.ui.R
4952
import com.fsck.k9.ui.base.extensions.withArguments
5053
import com.fsck.k9.ui.choosefolder.ChooseFolderActivity
@@ -55,6 +58,10 @@ import com.fsck.k9.ui.messageview.MessageCryptoPresenter.MessageCryptoMvpView
5558
import com.fsck.k9.ui.settings.account.AccountSettingsActivity
5659
import com.fsck.k9.ui.share.ShareIntentBuilder
5760
import java.util.Locale
61+
import kotlin.time.Instant
62+
import kotlinx.coroutines.launch
63+
import kotlinx.datetime.TimeZone
64+
import kotlinx.datetime.toLocalDateTime
5865
import net.thunderbird.core.android.account.LegacyAccountDto
5966
import net.thunderbird.core.android.account.LegacyAccountDtoManager
6067
import net.thunderbird.core.featureflag.FeatureFlagProvider
@@ -63,6 +70,8 @@ import net.thunderbird.core.preference.GeneralSettingsManager
6370
import net.thunderbird.core.ui.theme.api.Theme
6471
import net.thunderbird.core.ui.theme.manager.ThemeManager
6572
import net.thunderbird.feature.mail.folder.api.OutboxFolderManager
73+
import net.thunderbird.feature.mail.message.export.MessageExporter
74+
import net.thunderbird.feature.mail.message.export.MessageFileNameSuggester
6675
import org.koin.android.ext.android.inject
6776
import org.openintents.openpgp.util.OpenPgpIntentStarter
6877

@@ -102,6 +111,10 @@ class MessageViewFragment :
102111
private var showProgressThreshold: Long? = null
103112
private var preferredUnsubscribeUri: UnsubscribeUri? = null
104113

114+
private val messageExporter: MessageExporter by inject()
115+
116+
private val fileNameSuggester: MessageFileNameSuggester by inject()
117+
105118
/**
106119
* Used to temporarily store the destination folder for refile operations if a confirmation
107120
* dialog is shown.
@@ -117,6 +130,9 @@ class MessageViewFragment :
117130
private var isDeleteMenuItemDisabled: Boolean = false
118131
private var wasMessageMarkedAsOpened: Boolean = false
119132

133+
// Tracks whether the current Create Document flow is for exporting EML (and not for attachments)
134+
private var pendingEmlExport: Boolean = false
135+
120136
private var isActive: Boolean = false
121137
private set
122138

@@ -360,7 +376,11 @@ class MessageViewFragment :
360376
R.id.show_headers -> onShowHeaders()
361377
R.id.export_eml -> if (
362378
featureFlagProvider.provide(MessageViewFeatureFlags.ActionExportEml).isEnabled()
363-
) onExportEml() else return true
379+
) {
380+
onExportEml()
381+
} else {
382+
return true
383+
}
364384
else -> return false
365385
}
366386

@@ -638,6 +658,24 @@ class MessageViewFragment :
638658
if (uri == null) return
639659
require(uri.scheme == ContentResolver.SCHEME_CONTENT) { "content: URI required" }
640660

661+
if (pendingEmlExport) {
662+
// Handle EML export via exporter and reset flag regardless of outcome
663+
val exportUri = uri
664+
pendingEmlExport = false
665+
viewLifecycleOwner.lifecycleScope.launch {
666+
val ctx = requireContext()
667+
val rawUri = RawMessageProvider.getRawMessageUri(messageReference)
668+
val result = messageExporter.export(
669+
sourceUri = rawUri.toKmpUri(),
670+
destinationUri = exportUri.toKmpUri(),
671+
)
672+
if (result.isFailure) {
673+
Toast.makeText(ctx, R.string.message_view_status_attachment_not_saved, Toast.LENGTH_LONG).show()
674+
}
675+
}
676+
return
677+
}
678+
641679
createAttachmentController(currentAttachmentViewInfo).saveAttachmentTo(uri)
642680
}
643681

@@ -669,8 +707,31 @@ class MessageViewFragment :
669707
copyMessage(messageReference, destinationFolderId)
670708
}
671709

710+
@OptIn(kotlin.time.ExperimentalTime::class)
672711
private fun onExportEml() {
673-
// TODO trigger eml export
712+
// Mark this flow as an EML export so the result handler doesn't touch attachment logic
713+
pendingEmlExport = true
714+
val subject = message?.subject ?: ""
715+
val dateMillis = (message?.sentDate ?: message?.internalDate)?.time
716+
val localDateTime = if (dateMillis != null) {
717+
Instant.fromEpochMilliseconds(dateMillis)
718+
.toLocalDateTime(TimeZone.UTC)
719+
} else {
720+
// Fallback to current local time if message has no dates
721+
Instant.fromEpochMilliseconds(System.currentTimeMillis())
722+
.toLocalDateTime(TimeZone.currentSystemDefault())
723+
}
724+
val suggestedName = fileNameSuggester.suggestFileName(subject, localDateTime, "eml")
725+
try {
726+
createDocumentLauncher.launch(
727+
input = CreateDocumentResultContract.Input(
728+
title = suggestedName,
729+
mimeType = "message/rfc822",
730+
),
731+
)
732+
} catch (e: ActivityNotFoundException) {
733+
Toast.makeText(requireContext(), R.string.error_activity_not_found, Toast.LENGTH_LONG).show()
734+
}
674735
}
675736

676737
private fun onSendAlternate() {

0 commit comments

Comments
 (0)