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