Message View Redesign: Add bottom sheet (2)

There are still a few things missing:
- #6597
- #6598
- #6599

But this PR is already way too large. So the rest will be added in follow-up PRs.

https://user-images.githubusercontent.com/218061/214107017-dacc603e-0e7a-44ad-b2a0-fe8eac0e4ad5.mp4

Closes #6393

Message View Redesign: Add bottom sheet

Message View Redesign: Add bottom sheet
To display details about the sender and all recipients of a message, we want to display a bottom sheet (that can be dragged up to use the full screen height).

![Message header - Bottom Drawer HiFi](https://user-images.githubusercontent.com/218061/195583719-55fc6947-9e34-4add-836f-0041674ed6df.png)

When tapping the contact picture or name/email address, open the contacts app (if the correspondent is in the user's contacts, otherwise do nothing)

./gradlew :mail:protocols:imap:test --tests "com.fsck.k9.mail.store.imap.RealImapFolderTest" :app:storage:testDebugUnitTest --tests "com.fsck.k9.storage.messages.RetrieveMessageOperationsTest"

diff --git a/app/core/build.gradle b/app/core/build.gradle
index 9c8ca307969..46bba048477 100644
--- a/app/core/build.gradle
+++ b/app/core/build.gradle
@@ -25,6 +25,7 @@ dependencies {
     implementation libs.moshi
     implementation libs.timber
     implementation libs.mime4j.core
+    implementation libs.mime4j.dom

     testImplementation project(':mail:testing')
     testImplementation project(":backend:imap")
diff --git a/app/core/src/main/java/com/fsck/k9/helper/Contacts.java b/app/core/src/main/java/com/fsck/k9/helper/Contacts.java
index 2043e6bd5d8..d2642f7d044 100644
--- a/app/core/src/main/java/com/fsck/k9/helper/Contacts.java
+++ b/app/core/src/main/java/com/fsck/k9/helper/Contacts.java
@@ -1,6 +1,8 @@
 package com.fsck.k9.helper;

+import java.util.HashMap;
+
 import android.Manifest;
 import android.content.ContentResolver;
 import android.content.Context;
@@ -9,13 +11,12 @@
 import android.database.Cursor;
 import android.net.Uri;
 import android.provider.ContactsContract;
-import timber.log.Timber;
 import android.provider.ContactsContract.CommonDataKinds.Photo;
-import androidx.core.content.ContextCompat;

+import androidx.annotation.Nullable;
+import androidx.core.content.ContextCompat;
 import com.fsck.k9.mail.Address;
-
-import java.util.HashMap;
+import timber.log.Timber;

 /**
  * Helper class to access the contacts stored on the device.
@@ -37,7 +38,8 @@ public class Contacts {
             ContactsContract.CommonDataKinds.Email._ID,
             ContactsContract.Contacts.DISPLAY_NAME,
             ContactsContract.CommonDataKinds.Email.CONTACT_ID,
-            Photo.PHOTO_URI
+            Photo.PHOTO_URI,
+            ContactsContract.Contacts.LOOKUP_KEY
     };

     /**
@@ -52,6 +54,8 @@ public class Contacts {
      */
     protected static final int CONTACT_ID_INDEX = 2;

+    protected static final int LOOKUP_KEY_INDEX = 4;
+

     /**
      * Get instance of the Contacts class.
@@ -169,6 +173,24 @@ public boolean isAnyInContacts(final Address[] addresses) {
         return false;
     }

+    @Nullable
+    public Uri getContactUri(String emailAddress) {
+        Cursor cursor = getContactByAddress(emailAddress);
+        if (cursor == null) {
+            return null;
+        }
+
+        try (cursor) {
+            if (!cursor.moveToFirst()) {
+                return null;
+            }
+
+            long contactId = cursor.getLong(CONTACT_ID_INDEX);
+            String lookupKey = cursor.getString(LOOKUP_KEY_INDEX);
+            return ContactsContract.Contacts.getLookupUri(contactId, lookupKey);
+        }
+    }
+
     /**
      * Get the name of the contact an email address belongs to.
      *
diff --git a/app/core/src/main/java/com/fsck/k9/mailstore/MessageDetails.kt b/app/core/src/main/java/com/fsck/k9/mailstore/MessageDetails.kt
new file mode 100644
index 00000000000..19c14855f3e
--- /dev/null
+++ b/app/core/src/main/java/com/fsck/k9/mailstore/MessageDetails.kt
@@ -0,0 +1,22 @@
+package com.fsck.k9.mailstore
+
+import com.fsck.k9.mail.Address
+import java.util.Date
+
+data class MessageDetails(
+    val date: MessageDate,
+    val from: List<Address>,
+    val sender: Address?,
+    val replyTo: List<Address>,
+    val to: List<Address>,
+    val cc: List<Address>,
+    val bcc: List<Address>
+)
+
+sealed interface MessageDate {
+    data class ValidDate(val date: Date) : MessageDate
+
+    data class InvalidDate(val dateHeader: String) : MessageDate
+
+    object MissingDate : MessageDate
+}
diff --git a/app/core/src/main/java/com/fsck/k9/mailstore/MessageRepository.kt b/app/core/src/main/java/com/fsck/k9/mailstore/MessageRepository.kt
index aa88bc1e15d..e2fc950b16e 100644
--- a/app/core/src/main/java/com/fsck/k9/mailstore/MessageRepository.kt
+++ b/app/core/src/main/java/com/fsck/k9/mailstore/MessageRepository.kt
@@ -1,11 +1,69 @@
 package com.fsck.k9.mailstore

 import com.fsck.k9.controller.MessageReference
+import com.fsck.k9.mail.Address
 import com.fsck.k9.mail.Header
+import com.fsck.k9.mail.internet.MimeUtility
+import org.apache.james.mime4j.dom.field.DateTimeField
+import org.apache.james.mime4j.field.DefaultFieldParser

 class MessageRepository(private val messageStoreManager: MessageStoreManager) {
     fun getHeaders(messageReference: MessageReference): List<Header> {
         val messageStore = messageStoreManager.getMessageStore(messageReference.accountUuid)
         return messageStore.getHeaders(messageReference.folderId, messageReference.uid)
     }
+
+    fun getMessageDetails(messageReference: MessageReference): MessageDetails {
+        val messageStore = messageStoreManager.getMessageStore(messageReference.accountUuid)
+        val headers = messageStore.getHeaders(messageReference.folderId, messageReference.uid, MESSAGE_DETAILS_HEADERS)
+
+        val messageDate = headers.parseDate("date")
+        val fromAddresses = headers.parseAddresses("from")
+        val senderAddresses = headers.parseAddresses("sender")
+        val replyToAddresses = headers.parseAddresses("reply-to")
+        val toAddresses = headers.parseAddresses("to")
+        val ccAddresses = headers.parseAddresses("cc")
+        val bccAddresses = headers.parseAddresses("bcc")
+
+        return MessageDetails(
+            date = messageDate,
+            from = fromAddresses,
+            sender = senderAddresses.firstOrNull(),
+            replyTo = replyToAddresses,
+            to = toAddresses,
+            cc = ccAddresses,
+            bcc = bccAddresses
+        )
+    }
+
+    private fun List<Header>.firstHeaderOrNull(name: String): String? {
+        return firstOrNull { it.name.equals(name, ignoreCase = true) }?.value
+    }
+
+    private fun List<Header>.parseAddresses(headerName: String): List<Address> {
+        return Address.parse(MimeUtility.unfold(firstHeaderOrNull(headerName))).toList()
+    }
+
+    private fun List<Header>.parseDate(headerName: String): MessageDate {
+        val dateHeader = firstHeaderOrNull(headerName) ?: return MessageDate.MissingDate
+
+        return try {
+            val dateTimeField = DefaultFieldParser.parse("Date: $dateHeader") as DateTimeField
+            return MessageDate.ValidDate(date = dateTimeField.date)
+        } catch (e: Exception) {
+            MessageDate.InvalidDate(dateHeader)
+        }
+    }
+
+    companion object {
+        private val MESSAGE_DETAILS_HEADERS = setOf(
+            "date",
+            "from",
+            "sender",
+            "reply-to",
+            "to",
+            "cc",
+            "bcc",
+        )
+    }
 }
diff --git a/app/core/src/main/java/com/fsck/k9/mailstore/MessageStore.kt b/app/core/src/main/java/com/fsck/k9/mailstore/MessageStore.kt
index 75c292e237e..b7b5e88d612 100644
--- a/app/core/src/main/java/com/fsck/k9/mailstore/MessageStore.kt
+++ b/app/core/src/main/java/com/fsck/k9/mailstore/MessageStore.kt
@@ -155,6 +155,11 @@ interface MessageStore {
      */
     fun getHeaders(folderId: Long, messageServerId: String): List<Header>

+    /**
+     * Retrieve selected header fields of a message.
+     */
+    fun getHeaders(folderId: Long, messageServerId: String, headerNames: Set<String>): List<Header>
+
     /**
      * Return the size of this message store in bytes.
      */
diff --git a/app/storage/src/main/java/com/fsck/k9/storage/messages/K9MessageStore.kt b/app/storage/src/main/java/com/fsck/k9/storage/messages/K9MessageStore.kt
index ca1e7dec6ab..aeecdc0352b 100644
--- a/app/storage/src/main/java/com/fsck/k9/storage/messages/K9MessageStore.kt
+++ b/app/storage/src/main/java/com/fsck/k9/storage/messages/K9MessageStore.kt
@@ -132,6 +132,10 @@ class K9MessageStore(
         return retrieveMessageOperations.getHeaders(folderId, messageServerId)
     }

+    override fun getHeaders(folderId: Long, messageServerId: String, headerNames: Set<String>): List<Header> {
+        return retrieveMessageOperations.getHeaders(folderId, messageServerId, headerNames)
+    }
+
     override fun destroyMessages(folderId: Long, messageServerIds: Collection<String>) {
         deleteMessageOperations.destroyMessages(folderId, messageServerIds)
     }
diff --git a/app/storage/src/main/java/com/fsck/k9/storage/messages/RetrieveMessageOperations.kt b/app/storage/src/main/java/com/fsck/k9/storage/messages/RetrieveMessageOperations.kt
index c094ae3b3c2..041dda74745 100644
--- a/app/storage/src/main/java/com/fsck/k9/storage/messages/RetrieveMessageOperations.kt
+++ b/app/storage/src/main/java/com/fsck/k9/storage/messages/RetrieveMessageOperations.kt
@@ -2,6 +2,7 @@ package com.fsck.k9.storage.messages

 import androidx.core.database.getLongOrNull
 import com.fsck.k9.K9
+import com.fsck.k9.helper.mapToSet
 import com.fsck.k9.mail.Flag
 import com.fsck.k9.mail.Header
 import com.fsck.k9.mail.internet.MimeHeader
@@ -167,7 +168,7 @@ internal class RetrieveMessageOperations(private val lockableDatabase: LockableD
         }
     }

-    fun getHeaders(folderId: Long, messageServerId: String): List<Header> {
+    fun getHeaders(folderId: Long, messageServerId: String, headerNames: Set<String>? = null): List<Header> {
         return lockableDatabase.execute(false) { database ->
             database.rawQuery(
                 "SELECT message_parts.header FROM messages" +
@@ -178,10 +179,13 @@ internal class RetrieveMessageOperations(private val lockableDatabase: LockableD
                 if (!cursor.moveToFirst()) throw MessageNotFoundException(folderId, messageServerId)

                 val headerBytes = cursor.getBlob(0)
+                val lowercaseHeaderNames = headerNames?.mapToSet(headerNames.size) { it.lowercase() }

                 val header = MimeHeader()
                 MessageHeaderParser.parse(headerBytes.inputStream()) { name, value ->
-                    header.addRawHeader(name, value)
+                    if (lowercaseHeaderNames == null || name.lowercase() in lowercaseHeaderNames) {
+                        header.addRawHeader(name, value)
+                    }
                 }

                 header.headers
diff --git a/app/ui/legacy/src/main/java/com/fsck/k9/UiKoinModules.kt b/app/ui/legacy/src/main/java/com/fsck/k9/UiKoinModules.kt
index 79f928ec363..9ae3e125bc6 100644
--- a/app/ui/legacy/src/main/java/com/fsck/k9/UiKoinModules.kt
+++ b/app/ui/legacy/src/main/java/com/fsck/k9/UiKoinModules.kt
@@ -11,6 +11,7 @@ import com.fsck.k9.ui.choosefolder.chooseFolderUiModule
 import com.fsck.k9.ui.endtoend.endToEndUiModule
 import com.fsck.k9.ui.folders.foldersUiModule
 import com.fsck.k9.ui.managefolders.manageFoldersUiModule
+import com.fsck.k9.ui.messagedetails.messageDetailsUiModule
 import com.fsck.k9.ui.messagelist.messageListUiModule
 import com.fsck.k9.ui.messagesource.messageSourceModule
 import com.fsck.k9.ui.settings.settingsUiModule
@@ -33,5 +34,6 @@ val uiModules = listOf(
     viewModule,
     changelogUiModule,
     messageSourceModule,
-    accountUiModule
+    accountUiModule,
+    messageDetailsUiModule
 )
diff --git a/app/ui/legacy/src/main/java/com/fsck/k9/ui/messagedetails/AddToContactsLauncher.kt b/app/ui/legacy/src/main/java/com/fsck/k9/ui/messagedetails/AddToContactsLauncher.kt
new file mode 100644
index 00000000000..399883c40c5
--- /dev/null
+++ b/app/ui/legacy/src/main/java/com/fsck/k9/ui/messagedetails/AddToContactsLauncher.kt
@@ -0,0 +1,23 @@
+package com.fsck.k9.ui.messagedetails
+
+import android.content.Context
+import android.content.Intent
+import android.provider.ContactsContract
+
+internal class AddToContactsLauncher {
+    fun launch(context: Context, name: String?, email: String) {
+        val intent = Intent(Intent.ACTION_INSERT).apply {
+            type = ContactsContract.Contacts.CONTENT_TYPE
+
+            putExtra(ContactsContract.Intents.Insert.EMAIL, email)
+
+            if (name != null) {
+                putExtra(ContactsContract.Intents.Insert.NAME, name)
+            }
+
+            flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_NEW_DOCUMENT
+        }
+
+        context.startActivity(intent)
+    }
+}
diff --git a/app/ui/legacy/src/main/java/com/fsck/k9/ui/messagedetails/ContactSettingsProvider.kt b/app/ui/legacy/src/main/java/com/fsck/k9/ui/messagedetails/ContactSettingsProvider.kt
new file mode 100644
index 00000000000..eefb56e8e00
--- /dev/null
+++ b/app/ui/legacy/src/main/java/com/fsck/k9/ui/messagedetails/ContactSettingsProvider.kt
@@ -0,0 +1,8 @@
+package com.fsck.k9.ui.messagedetails
+
+import com.fsck.k9.K9
+
+class ContactSettingsProvider {
+    val isShowContactPicture: Boolean
+        get() = K9.isShowContactPicture
+}
diff --git a/app/ui/legacy/src/main/java/com/fsck/k9/ui/messagedetails/KoinModule.kt b/app/ui/legacy/src/main/java/com/fsck/k9/ui/messagedetails/KoinModule.kt
new file mode 100644
index 00000000000..75f6cd59572
--- /dev/null
+++ b/app/ui/legacy/src/main/java/com/fsck/k9/ui/messagedetails/KoinModule.kt
@@ -0,0 +1,19 @@
+package com.fsck.k9.ui.messagedetails
+
+import org.koin.androidx.viewmodel.dsl.viewModel
+import org.koin.dsl.module
+
+val messageDetailsUiModule = module {
+    viewModel {
+        MessageDetailsViewModel(
+            resources = get(),
+            messageRepository = get(),
+            contactSettingsProvider = get(),
+            contacts = get(),
+            clipboardManager = get()
+        )
+    }
+    factory { ContactSettingsProvider() }
+    factory { AddToContactsLauncher() }
+    factory { ShowContactLauncher() }
+}
diff --git a/app/ui/legacy/src/main/java/com/fsck/k9/ui/messagedetails/MessageDateItem.kt b/app/ui/legacy/src/main/java/com/fsck/k9/ui/messagedetails/MessageDateItem.kt
new file mode 100644
index 00000000000..0c5a52d7f35
--- /dev/null
+++ b/app/ui/legacy/src/main/java/com/fsck/k9/ui/messagedetails/MessageDateItem.kt
@@ -0,0 +1,26 @@
+package com.fsck.k9.ui.messagedetails
+
+import android.view.View
+import android.widget.TextView
+import com.fsck.k9.ui.R
+import com.mikepenz.fastadapter.FastAdapter
+import com.mikepenz.fastadapter.items.AbstractItem
+
+internal class MessageDateItem(private val date: String) : AbstractItem<MessageDateItem.ViewHolder>() {
+    override val type: Int = R.id.message_details_date
+    override val layoutRes = R.layout.message_details_date_item
+
+    override fun getViewHolder(v: View) = ViewHolder(v)
+
+    class ViewHolder(view: View) : FastAdapter.ViewHolder<MessageDateItem>(view) {
+        private val textView = view.findViewById<TextView>(R.id.date)
+
+        override fun bindView(item: MessageDateItem, payloads: List<Any>) {
+            textView.text = item.date
+        }
+
+        override fun unbindView(item: MessageDateItem) {
+            textView.text = null
+        }
+    }
+}
diff --git a/app/ui/legacy/src/main/java/com/fsck/k9/ui/messagedetails/MessageDetailsDividerItem.kt b/app/ui/legacy/src/main/java/com/fsck/k9/ui/messagedetails/MessageDetailsDividerItem.kt
new file mode 100644
index 00000000000..9706b2d4b29
--- /dev/null
+++ b/app/ui/legacy/src/main/java/com/fsck/k9/ui/messagedetails/MessageDetailsDividerItem.kt
@@ -0,0 +1,19 @@
+package com.fsck.k9.ui.messagedetails
+
+import android.view.View
+import com.fsck.k9.ui.R
+import com.mikepenz.fastadapter.FastAdapter
+import com.mikepenz.fastadapter.items.AbstractItem
+
+internal class MessageDetailsDividerItem : AbstractItem<MessageDetailsDividerItem.ViewHolder>() {
+    override val type: Int = R.id.message_details_divider
+    override val layoutRes = R.layout.message_details_divider_item
+
+    override fun getViewHolder(v: View) = ViewHolder(v)
+
+    class ViewHolder(view: View) : FastAdapter.ViewHolder<MessageDetailsDividerItem>(view) {
+        override fun bindView(item: MessageDetailsDividerItem, payloads: List<Any>) = Unit
+
+        override fun unbindView(item: MessageDetailsDividerItem) = Unit
+    }
+}
diff --git a/app/ui/legacy/src/main/java/com/fsck/k9/ui/messagedetails/MessageDetailsFragment.kt b/app/ui/legacy/src/main/java/com/fsck/k9/ui/messagedetails/MessageDetailsFragment.kt
new file mode 100644
index 00000000000..5f877dc2158
--- /dev/null
+++ b/app/ui/legacy/src/main/java/com/fsck/k9/ui/messagedetails/MessageDetailsFragment.kt
@@ -0,0 +1,249 @@
+package com.fsck.k9.ui.messagedetails
+
+import android.content.Intent
+import android.net.Uri
+import android.os.Bundle
+import android.view.LayoutInflater
+import android.view.MenuItem
+import android.view.View
+import android.view.ViewGroup
+import android.widget.ProgressBar
+import androidx.annotation.StringRes
+import androidx.appcompat.widget.PopupMenu
+import androidx.core.content.ContextCompat
+import androidx.core.view.isVisible
+import androidx.recyclerview.widget.RecyclerView
+import app.k9mail.ui.utils.bottomsheet.ToolbarBottomSheetDialogFragment
+import com.fsck.k9.activity.MessageCompose
+import com.fsck.k9.contacts.ContactPictureLoader
+import com.fsck.k9.controller.MessageReference
+import com.fsck.k9.mail.Address
+import com.fsck.k9.ui.R
+import com.fsck.k9.ui.observe
+import com.fsck.k9.ui.withArguments
+import com.mikepenz.fastadapter.FastAdapter
+import com.mikepenz.fastadapter.GenericItem
+import com.mikepenz.fastadapter.adapters.ItemAdapter
+import com.mikepenz.fastadapter.listeners.ClickEventHook
+import org.koin.android.ext.android.inject
+import org.koin.androidx.viewmodel.ext.android.viewModel
+
+class MessageDetailsFragment : ToolbarBottomSheetDialogFragment() {
+    private val viewModel: MessageDetailsViewModel by viewModel()
+    private val addToContactsLauncher: AddToContactsLauncher by inject()
+    private val showContactLauncher: ShowContactLauncher by inject()
+    private val contactPictureLoader: ContactPictureLoader by inject()
+
+    private lateinit var messageReference: MessageReference
+
+    override fun onCreate(savedInstanceState: Bundle?) {
+        super.onCreate(savedInstanceState)
+
+        messageReference = MessageReference.parse(arguments?.getString(ARG_REFERENCE))
+            ?: error("Missing argument $ARG_REFERENCE")
+    }
+
+    override fun onCreateView(
+        inflater: LayoutInflater,
+        container: ViewGroup?,
+        savedInstanceState: Bundle?
+    ): View? {
+        return inflater.inflate(R.layout.message_bottom_sheet, container, false)
+    }
+
+    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
+        super.onViewCreated(view, savedInstanceState)
+
+        val dialog = checkNotNull(dialog)
+        dialog.isDismissWithAnimation = true
+
+        val toolbar = checkNotNull(toolbar)
+        toolbar.apply {
+            title = getString(R.string.message_details_toolbar_title)
+            navigationIcon = ContextCompat.getDrawable(requireContext(), R.drawable.ic_close)
+
+            setNavigationOnClickListener {
+                dismiss()
+            }
+        }
+
+        val progressBar = view.findViewById<ProgressBar>(R.id.message_details_progress)
+        val errorView = view.findViewById<View>(R.id.message_details_error)
+        val recyclerView = view.findViewById<RecyclerView>(R.id.message_details_list)
+
+        viewModel.loadData(messageReference).observe(this) { state ->
+            when (state) {
+                MessageDetailsState.Loading -> {
+                    progressBar.isVisible = true
+                    errorView.isVisible = false
+                    recyclerView.isVisible = false
+                }
+                MessageDetailsState.Error -> {
+                    progressBar.isVisible = false
+                    errorView.isVisible = true
+                    recyclerView.isVisible = false
+                }
+                is MessageDetailsState.DataLoaded -> {
+                    progressBar.isVisible = false
+                    errorView.isVisible = false
+                    recyclerView.isVisible = true
+                    setMessageDetails(recyclerView, state.details, state.showContactPicture)
+                }
+            }
+        }
+    }
+
+    private fun setMessageDetails(recyclerView: RecyclerView, details: MessageDetailsUi, showContactPicture: Boolean) {
+        val itemAdapter = ItemAdapter<GenericItem>().apply {
+            add(MessageDateItem(details.date ?: getString(R.string.message_details_missing_date)))
+
+            addParticipants(details.from, R.string.message_details_from_section_title, showContactPicture)
+            addParticipants(details.sender, R.string.message_details_sender_section_title, showContactPicture)
+            addParticipants(details.replyTo, R.string.message_details_replyto_section_title, showContactPicture)
+
+            add(MessageDetailsDividerItem())
+
+            addParticipants(details.to, R.string.message_details_to_section_title, showContactPicture)
+            addParticipants(details.cc, R.string.message_details_cc_section_title, showContactPicture)
+            addParticipants(details.bcc, R.string.message_details_bcc_section_title, showContactPicture)
+        }
+
+        val adapter = FastAdapter.with(itemAdapter).apply {
+            addEventHook(participantClickEventHook)
+            addEventHook(addToContactsClickEventHook)
+            addEventHook(composeClickEventHook)
+            addEventHook(overflowClickEventHook)
+        }
+
+        recyclerView.adapter = adapter
+    }
+
+    private fun ItemAdapter<GenericItem>.addParticipants(
+        participants: List<Participant>,
+        @StringRes title: Int,
+        showContactPicture: Boolean
+    ) {
+        if (participants.isNotEmpty()) {
+            val extraText = if (participants.size > 1) participants.size.toString() else null
+            add(SectionHeaderItem(title = getString(title), extra = extraText))
+
+            for (participant in participants) {
+                add(ParticipantItem(contactPictureLoader, showContactPicture, participant))
+            }
+        }
+    }
+
+    private val participantClickEventHook = object : ClickEventHook<ParticipantItem>() {
+        override fun onBind(viewHolder: RecyclerView.ViewHolder): View? {
+            return if (viewHolder is ParticipantItem.ViewHolder) {
+                viewHolder.itemView
+            } else {
+                null
+            }
+        }
+
+        override fun onClick(v: View, position: Int, fastAdapter: FastAdapter<ParticipantItem>, item: ParticipantItem) {
+            val contactLookupUri = item.participant.contactLookupUri ?: return
+            showContact(contactLookupUri)
+        }
+    }
+
+    private fun showContact(contactLookupUri: Uri) {
+        showContactLauncher.launch(requireContext(), contactLookupUri)
+    }
+
+    private val addToContactsClickEventHook = object : ClickEventHook<ParticipantItem>() {
+        override fun onBind(viewHolder: RecyclerView.ViewHolder): View? {
+            return if (viewHolder is ParticipantItem.ViewHolder) {
+                viewHolder.menuAddContact
+            } else {
+                null
+            }
+        }
+
+        override fun onClick(v: View, position: Int, fastAdapter: FastAdapter<ParticipantItem>, item: ParticipantItem) {
+            val address = item.participant.address
+            addToContacts(address)
+        }
+    }
+
+    private fun addToContacts(address: Address) {
+        addToContactsLauncher.launch(context = requireContext(), name = address.personal, email = address.address)
+    }
+
+    private val composeClickEventHook = object : ClickEventHook<ParticipantItem>() {
+        override fun onBind(viewHolder: RecyclerView.ViewHolder): View? {
+            return if (viewHolder is ParticipantItem.ViewHolder) {
+                viewHolder.menuCompose
+            } else {
+                null
+            }
+        }
+
+        override fun onClick(v: View, position: Int, fastAdapter: FastAdapter<ParticipantItem>, item: ParticipantItem) {
+            val address = item.participant.address
+            composeMessageToAddress(address)
+        }
+    }
+
+    private fun composeMessageToAddress(address: Address) {
+        // TODO: Use the identity this message was sent to as sender identity
+
+        val intent = Intent(context, MessageCompose::class.java).apply {
+            action = Intent.ACTION_SEND
+            putExtra(Intent.EXTRA_EMAIL, arrayOf(address.toString()))
+            putExtra(MessageCompose.EXTRA_ACCOUNT, messageReference.accountUuid)
+        }
+
+        dismiss()
+        requireContext().startActivity(intent)
+    }
+
+    private val overflowClickEventHook = object : ClickEventHook<ParticipantItem>() {
+        override fun onBind(viewHolder: RecyclerView.ViewHolder): View? {
+            return if (viewHolder is ParticipantItem.ViewHolder) {
+                viewHolder.menuOverflow
+            } else {
+                null
+            }
+        }
+
+        override fun onClick(v: View, position: Int, fastAdapter: FastAdapter<ParticipantItem>, item: ParticipantItem) {
+            showOverflowMenu(v, item.participant)
+        }
+    }
+
+    private fun showOverflowMenu(view: View, participant: Participant) {
+        val popupMenu = PopupMenu(requireContext(), view).apply {
+            inflate(R.menu.participant_overflow_menu)
+        }
+
+        if (participant.address.personal == null) {
+            popupMenu.menu.findItem(R.id.copy_name_and_email_address).isVisible = false
+        }
+
+        popupMenu.setOnMenuItemClickListener { item: MenuItem ->
+            onOverflowMenuItemClick(item.itemId, participant)
+            true
+        }
+
+        popupMenu.show()
+    }
+
+    private fun onOverflowMenuItemClick(itemId: Int, participant: Participant) {
+        when (itemId) {
+            R.id.copy_email_address -> viewModel.onCopyEmailAddressToClipboard(participant)
+            R.id.copy_name_and_email_address -> viewModel.onCopyNameAndEmailAddressToClipboard(participant)
+        }
+    }
+
+    companion object {
+        private const val ARG_REFERENCE = "reference"
+
+        fun create(messageReference: MessageReference): MessageDetailsFragment {
+            return MessageDetailsFragment().withArguments(
+                ARG_REFERENCE to messageReference.toIdentityString()
+            )
+        }
+    }
+}
diff --git a/app/ui/legacy/src/main/java/com/fsck/k9/ui/messagedetails/MessageDetailsUi.kt b/app/ui/legacy/src/main/java/com/fsck/k9/ui/messagedetails/MessageDetailsUi.kt
new file mode 100644
index 00000000000..e157e41a975
--- /dev/null
+++ b/app/ui/legacy/src/main/java/com/fsck/k9/ui/messagedetails/MessageDetailsUi.kt
@@ -0,0 +1,22 @@
+package com.fsck.k9.ui.messagedetails
+
+import android.net.Uri
+import com.fsck.k9.mail.Address
+
+data class MessageDetailsUi(
+    val date: String?,
+    val from: List<Participant>,
+    val sender: List<Participant>,
+    val replyTo: List<Participant>,
+    val to: List<Participant>,
+    val cc: List<Participant>,
+    val bcc: List<Participant>
+)
+
+data class Participant(
+    val address: Address,
+    val contactLookupUri: Uri?
+) {
+    val isInContacts: Boolean
+        get() = contactLookupUri != null
+}
diff --git a/app/ui/legacy/src/main/java/com/fsck/k9/ui/messagedetails/MessageDetailsViewModel.kt b/app/ui/legacy/src/main/java/com/fsck/k9/ui/messagedetails/MessageDetailsViewModel.kt
new file mode 100644
index 00000000000..e67ee44fcea
--- /dev/null
+++ b/app/ui/legacy/src/main/java/com/fsck/k9/ui/messagedetails/MessageDetailsViewModel.kt
@@ -0,0 +1,95 @@
+package com.fsck.k9.ui.messagedetails
+
+import android.content.res.Resources
+import androidx.lifecycle.ViewModel
+import androidx.lifecycle.viewModelScope
+import com.fsck.k9.controller.MessageReference
+import com.fsck.k9.helper.ClipboardManager
+import com.fsck.k9.helper.Contacts
+import com.fsck.k9.mail.Address
+import com.fsck.k9.mailstore.MessageDate
+import com.fsck.k9.mailstore.MessageRepository
+import com.fsck.k9.ui.R
+import java.text.DateFormat
+import java.util.Locale
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.StateFlow
+import kotlinx.coroutines.launch
+
+internal class MessageDetailsViewModel(
+    private val resources: Resources,
+    private val messageRepository: MessageRepository,
+    private val contactSettingsProvider: ContactSettingsProvider,
+    private val contacts: Contacts,
+    private val clipboardManager: ClipboardManager
+) : ViewModel() {
+    private val dateFormat = DateFormat.getDateTimeInstance(DateFormat.LONG, DateFormat.MEDIUM, Locale.getDefault())
+    private val uiState = MutableStateFlow<MessageDetailsState>(MessageDetailsState.Loading)
+
+    fun loadData(messageReference: MessageReference): StateFlow<MessageDetailsState> {
+        viewModelScope.launch(Dispatchers.IO) {
+            uiState.value = try {
+                val messageDetails = messageRepository.getMessageDetails(messageReference)
+
+                val senderList = messageDetails.sender?.let { listOf(it) } ?: emptyList()
+                val messageDetailsUi = MessageDetailsUi(
+                    date = buildDisplayDate(messageDetails.date),
+                    from = messageDetails.from.toParticipants(),
+                    sender = senderList.toParticipants(),
+                    replyTo = messageDetails.replyTo.toParticipants(),
+                    to = messageDetails.to.toParticipants(),
+                    cc = messageDetails.cc.toParticipants(),
+                    bcc = messageDetails.bcc.toParticipants()
+                )
+
+                MessageDetailsState.DataLoaded(
+                    showContactPicture = contactSettingsProvider.isShowContactPicture,
+                    details = messageDetailsUi
+                )
+            } catch (e: Exception) {
+                MessageDetailsState.Error
+            }
+        }
+
+        return uiState
+    }
+
+    private fun buildDisplayDate(messageDate: MessageDate): String? {
+        return when (messageDate) {
+            is MessageDate.InvalidDate -> messageDate.dateHeader
+            MessageDate.MissingDate -> null
+            is MessageDate.ValidDate -> dateFormat.format(messageDate.date)
+        }
+    }
+
+    private fun List<Address>.toParticipants(): List<Participant> {
+        return this.map { address ->
+            Participant(
+                address = address,
+                contactLookupUri = contacts.getContactUri(address.address)
+            )
+        }
+    }
+
+    fun onCopyEmailAddressToClipboard(participant: Participant) {
+        val label = resources.getString(R.string.clipboard_label_email_address)
+        val emailAddress = participant.address.address
+        clipboardManager.setText(label, emailAddress)
+    }
+
+    fun onCopyNameAndEmailAddressToClipboard(participant: Participant) {
+        val label = resources.getString(R.string.clipboard_label_name_and_email_address)
+        val nameAndEmailAddress = participant.address.toString()
+        clipboardManager.setText(label, nameAndEmailAddress)
+    }
+}
+
+sealed interface MessageDetailsState {
+    object Loading : MessageDetailsState
+    object Error : MessageDetailsState
+    data class DataLoaded(
+        val showContactPicture: Boolean,
+        val details: MessageDetailsUi
+    ) : MessageDetailsState
+}
diff --git a/app/ui/legacy/src/main/java/com/fsck/k9/ui/messagedetails/ParticipantItem.kt b/app/ui/legacy/src/main/java/com/fsck/k9/ui/messagedetails/ParticipantItem.kt
new file mode 100644
index 00000000000..51800bd8c0d
--- /dev/null
+++ b/app/ui/legacy/src/main/java/com/fsck/k9/ui/messagedetails/ParticipantItem.kt
@@ -0,0 +1,74 @@
+package com.fsck.k9.ui.messagedetails
+
+import android.view.View
+import android.widget.ImageView
+import android.widget.TextView
+import androidx.appcompat.widget.TooltipCompat
+import androidx.core.view.isVisible
+import com.fsck.k9.contacts.ContactPictureLoader
+import com.fsck.k9.ui.R
+import com.mikepenz.fastadapter.FastAdapter
+import com.mikepenz.fastadapter.items.AbstractItem
+
+internal class ParticipantItem(
+    private val contactPictureLoader: ContactPictureLoader,
+    private val showContactsPicture: Boolean,
+    val participant: Participant
+) : AbstractItem<ParticipantItem.ViewHolder>() {
+    override val type: Int = R.id.message_details_participant
+    override val layoutRes = R.layout.message_details_participant_item
+
+    override fun getViewHolder(v: View) = ViewHolder(v)
+
+    class ViewHolder(view: View) : FastAdapter.ViewHolder<ParticipantItem>(view) {
+        val menuAddContact: View = view.findViewById(R.id.menu_add_contact)
+        val menuCompose: View = view.findViewById(R.id.menu_compose)
+        val menuOverflow: View = view.findViewById(R.id.menu_overflow)
+
+        private val contactPicture: ImageView = view.findViewById(R.id.contact_picture)
+        private val name = view.findViewById<TextView>(R.id.name)
+        private val email = view.findViewById<TextView>(R.id.email)
+        private val originalBackground = view.background
+
+        init {
+            TooltipCompat.setTooltipText(menuAddContact, menuAddContact.contentDescription)
+            TooltipCompat.setTooltipText(menuCompose, menuCompose.contentDescription)
+            TooltipCompat.setTooltipText(menuOverflow, menuOverflow.contentDescription)
+        }
+
+        override fun bindView(item: ParticipantItem, payloads: List<Any>) {
+            val participant = item.participant
+            val address = participant.address
+            val participantName = address.personal
+
+            if (participantName != null) {
+                name.text = participantName
+                email.text = address.address
+            } else {
+                name.text = address.address
+                email.isVisible = false
+            }
+            menuAddContact.isVisible = !participant.isInContacts
+
+            if (item.showContactsPicture) {
+                item.contactPictureLoader.setContactPicture(contactPicture, address)
+            } else {
+                contactPicture.isVisible = false
+            }
+
+            if (!item.participant.isInContacts) {
+                itemView.isClickable = false
+                itemView.background = null
+            }
+        }
+
+        override fun unbindView(item: ParticipantItem) {
+            name.text = null
+            email.text = null
+            email.isVisible = true
+            contactPicture.isVisible = true
+            itemView.background = originalBackground
+            itemView.isClickable = true
+        }
+    }
+}
diff --git a/app/ui/legacy/src/main/java/com/fsck/k9/ui/messagedetails/SectionHeaderItem.kt b/app/ui/legacy/src/main/java/com/fsck/k9/ui/messagedetails/SectionHeaderItem.kt
new file mode 100644
index 00000000000..487aba72907
--- /dev/null
+++ b/app/ui/legacy/src/main/java/com/fsck/k9/ui/messagedetails/SectionHeaderItem.kt
@@ -0,0 +1,32 @@
+package com.fsck.k9.ui.messagedetails
+
+import android.view.View
+import android.widget.TextView
+import com.fsck.k9.ui.R
+import com.mikepenz.fastadapter.FastAdapter
+import com.mikepenz.fastadapter.items.AbstractItem
+
+internal class SectionHeaderItem(
+    private val title: String,
+    private val extra: String?
+) : AbstractItem<SectionHeaderItem.ViewHolder>() {
+    override val type: Int = R.id.message_details_section_header
+    override val layoutRes = R.layout.message_details_section_header_item
+
+    override fun getViewHolder(v: View) = ViewHolder(v)
+
+    class ViewHolder(view: View) : FastAdapter.ViewHolder<SectionHeaderItem>(view) {
+        private val textView = view.findViewById<TextView>(R.id.title)
+        private val extraTextView = view.findViewById<TextView>(R.id.extra)
+
+        override fun bindView(item: SectionHeaderItem, payloads: List<Any>) {
+            textView.text = item.title
+            extraTextView.text = item.extra
+        }
+
+        override fun unbindView(item: SectionHeaderItem) {
+            textView.text = null
+            extraTextView.text = null
+        }
+    }
+}
diff --git a/app/ui/legacy/src/main/java/com/fsck/k9/ui/messagedetails/ShowContactLauncher.kt b/app/ui/legacy/src/main/java/com/fsck/k9/ui/messagedetails/ShowContactLauncher.kt
new file mode 100644
index 00000000000..b6ef5d799ab
--- /dev/null
+++ b/app/ui/legacy/src/main/java/com/fsck/k9/ui/messagedetails/ShowContactLauncher.kt
@@ -0,0 +1,16 @@
+package com.fsck.k9.ui.messagedetails
+
+import android.content.Context
+import android.content.Intent
+import android.net.Uri
+
+internal class ShowContactLauncher {
+    fun launch(context: Context, contactLookupUri: Uri) {
+        val intent = Intent(Intent.ACTION_VIEW).apply {
+            data = contactLookupUri
+            flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_NEW_DOCUMENT
+        }
+
+        context.startActivity(intent)
+    }
+}
diff --git a/app/ui/legacy/src/main/java/com/fsck/k9/ui/messageview/MessageBottomSheet.kt b/app/ui/legacy/src/main/java/com/fsck/k9/ui/messageview/MessageBottomSheet.kt
deleted file mode 100644
index a32bc6a531f..00000000000
--- a/app/ui/legacy/src/main/java/com/fsck/k9/ui/messageview/MessageBottomSheet.kt
+++ /dev/null
@@ -1,36 +0,0 @@
-package com.fsck.k9.ui.messageview
-
-import android.os.Bundle
-import android.view.LayoutInflater
-import android.view.View
-import android.view.ViewGroup
-import androidx.core.content.ContextCompat
-import app.k9mail.ui.utils.bottomsheet.ToolbarBottomSheetDialogFragment
-import com.fsck.k9.ui.R
-
-class MessageBottomSheet : ToolbarBottomSheetDialogFragment() {
-    override fun onCreateView(
-        inflater: LayoutInflater,
-        container: ViewGroup?,
-        savedInstanceState: Bundle?
-    ): View? {
-        return inflater.inflate(R.layout.message_bottom_sheet, container, false)
-    }
-
-    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
-        super.onViewCreated(view, savedInstanceState)
-
-        val dialog = checkNotNull(dialog)
-        dialog.isDismissWithAnimation = true
-
-        val toolbar = checkNotNull(toolbar)
-        toolbar.apply {
-            title = "Message details"
-            navigationIcon = ContextCompat.getDrawable(requireContext(), R.drawable.ic_close)
-
-            setNavigationOnClickListener {
-                dismiss()
-            }
-        }
-    }
-}
diff --git a/app/ui/legacy/src/main/java/com/fsck/k9/ui/messageview/MessageViewFragment.kt b/app/ui/legacy/src/main/java/com/fsck/k9/ui/messageview/MessageViewFragment.kt
index dbe51061139..acb52cf40a3 100644
--- a/app/ui/legacy/src/main/java/com/fsck/k9/ui/messageview/MessageViewFragment.kt
+++ b/app/ui/legacy/src/main/java/com/fsck/k9/ui/messageview/MessageViewFragment.kt
@@ -45,6 +45,7 @@ import com.fsck.k9.ui.R
 import com.fsck.k9.ui.base.Theme
 import com.fsck.k9.ui.base.ThemeManager
 import com.fsck.k9.ui.choosefolder.ChooseFolderActivity
+import com.fsck.k9.ui.messagedetails.MessageDetailsFragment
 import com.fsck.k9.ui.messagesource.MessageSourceActivity
 import com.fsck.k9.ui.messageview.CryptoInfoDialog.OnClickShowCryptoKeyListener
 import com.fsck.k9.ui.messageview.MessageCryptoPresenter.MessageCryptoMvpView
@@ -382,8 +383,8 @@ class MessageViewFragment :

     private val messageHeaderClickListener = object : MessageHeaderClickListener {
         override fun onParticipantsContainerClick() {
-            val messageBottomSheet = MessageBottomSheet()
-            messageBottomSheet.show(childFragmentManager, "message_details")
+            val messageDetailsFragment = MessageDetailsFragment.create(messageReference)
+            messageDetailsFragment.show(childFragmentManager, "message_details")
         }

         override fun onMenuItemClick(itemId: Int) {
diff --git a/app/ui/legacy/src/main/res/layout/message_bottom_sheet.xml b/app/ui/legacy/src/main/res/layout/message_bottom_sheet.xml
index df07b9a486f..b97430c3e02 100644
--- a/app/ui/legacy/src/main/res/layout/message_bottom_sheet.xml
+++ b/app/ui/legacy/src/main/res/layout/message_bottom_sheet.xml
@@ -1,62 +1,36 @@
 <?xml version="1.0" encoding="utf-8"?>
-<androidx.core.widget.NestedScrollView xmlns:android="http://schemas.android.com/apk/res/android"
+<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:app="http://schemas.android.com/apk/res-auto"
+    xmlns:tools="http://schemas.android.com/tools"
     android:layout_width="match_parent"
-    android:layout_height="match_parent">
+    android:layout_height="wrap_content">

-    <LinearLayout
-        android:layout_width="match_parent"
+    <ProgressBar
+        android:id="@+id/message_details_progress"
+        style="?android:attr/progressBarStyle"
+        android:layout_width="wrap_content"
         android:layout_height="wrap_content"
-        android:orientation="vertical">
-
-
-        <TextView
-            android:layout_width="match_parent"
-            android:layout_height="wrap_content"
-            android:layout_marginTop="16dp"
-            android:layout_marginHorizontal="16dp"
-            android:textAppearance="?attr/textAppearanceBody2"
-            android:gravity="end"
-            android:text="Wednesday, 11 January 2023, 10:31 pm" />
-
-        <TextView
-            android:layout_width="match_parent"
-            android:layout_height="wrap_content"
-            android:layout_marginTop="16dp"
-            android:layout_marginHorizontal="16dp"
-            android:textAppearance="?attr/textAppearanceBody2"
-            android:textStyle="bold"
-            android:text="From" />
-        <include layout="@layout/participant_list_item"/>
-
-        <TextView
-            android:layout_width="match_parent"
-            android:layout_height="wrap_content"
-            android:layout_marginTop="16dp"
-            android:layout_marginHorizontal="16dp"
-            android:textAppearance="?attr/textAppearanceBody2"
-            android:textStyle="bold"
-            android:text="To" />
+        android:layout_gravity="center"
+        android:layout_marginVertical="32dp"
+        tools:visibility="gone" />

-        <include layout="@layout/participant_list_item"/>
-        <include layout="@layout/participant_list_item"/>
-
-        <TextView
-            android:layout_width="match_parent"
-            android:layout_height="wrap_content"
-            android:layout_marginTop="16dp"
-            android:layout_marginHorizontal="16dp"
-            android:textAppearance="?attr/textAppearanceBody2"
-            android:textStyle="bold"
-            android:text="CC" />
-
-        <include layout="@layout/participant_list_item"/>
-        <include layout="@layout/participant_list_item"/>
-        <include layout="@layout/participant_list_item"/>
-        <include layout="@layout/participant_list_item"/>
-        <include layout="@layout/participant_list_item"/>
-        <include layout="@layout/participant_list_item"/>
-        <include layout="@layout/participant_list_item"/>
-        <include layout="@layout/participant_list_item"/>
+    <TextView
+        android:id="@+id/message_details_error"
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:layout_marginHorizontal="16dp"
+        android:layout_marginVertical="32dp"
+        android:text="@string/message_details_loading_error"
+        android:visibility="gone"
+        tools:visibility="gone" />
+
+    <androidx.recyclerview.widget.RecyclerView
+        android:id="@+id/message_details_list"
+        android:layout_width="match_parent"
+        android:layout_height="match_parent"
+        android:overScrollMode="never"
+        android:visibility="gone"
+        app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
+        tools:visibility="visible" />

-    </LinearLayout>
-</androidx.core.widget.NestedScrollView>
+</FrameLayout>
diff --git a/app/ui/legacy/src/main/res/layout/message_details_date_item.xml b/app/ui/legacy/src/main/res/layout/message_details_date_item.xml
new file mode 100644
index 00000000000..2888a4294a7
--- /dev/null
+++ b/app/ui/legacy/src/main/res/layout/message_details_date_item.xml
@@ -0,0 +1,12 @@
+<?xml version="1.0" encoding="utf-8"?>
+<TextView xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:tools="http://schemas.android.com/tools"
+    android:id="@+id/date"
+    android:layout_width="match_parent"
+    android:layout_height="wrap_content"
+    android:layout_marginHorizontal="16dp"
+    android:layout_marginTop="16dp"
+    android:focusable="false"
+    android:gravity="end"
+    android:textAppearance="?attr/textAppearanceBody2"
+    tools:text="Wednesday, 11 January 2023, 10:31 pm" />
diff --git a/app/ui/legacy/src/main/res/layout/message_details_divider_item.xml b/app/ui/legacy/src/main/res/layout/message_details_divider_item.xml
new file mode 100644
index 00000000000..8a456ec9a6f
--- /dev/null
+++ b/app/ui/legacy/src/main/res/layout/message_details_divider_item.xml
@@ -0,0 +1,8 @@
+<?xml version="1.0" encoding="utf-8"?>
+<View xmlns:android="http://schemas.android.com/apk/res/android"
+    android:layout_width="match_parent"
+    android:layout_height="1px"
+    android:layout_marginHorizontal="32dp"
+    android:layout_marginVertical="8dp"
+    android:background="?attr/messageDetailsDividerColor"
+    android:focusable="false" />
diff --git a/app/ui/legacy/src/main/res/layout/participant_list_item.xml b/app/ui/legacy/src/main/res/layout/message_details_participant_item.xml
similarity index 82%
rename from app/ui/legacy/src/main/res/layout/participant_list_item.xml
rename to app/ui/legacy/src/main/res/layout/message_details_participant_item.xml
index b8cfc503415..ce99f941ffa 100644
--- a/app/ui/legacy/src/main/res/layout/participant_list_item.xml
+++ b/app/ui/legacy/src/main/res/layout/message_details_participant_item.xml
@@ -5,9 +5,7 @@
     android:id="@+id/participants_container"
     android:layout_width="match_parent"
     android:layout_height="wrap_content"
-    android:clickable="true"
-    android:focusable="true"
-    android:foreground="?attr/selectableItemBackground"
+    android:background="?attr/selectableItemBackground"
     android:gravity="center_vertical"
     android:orientation="horizontal">

@@ -15,13 +13,11 @@
         android:id="@+id/contact_picture"
         android:layout_width="40dp"
         android:layout_height="40dp"
-        android:layout_margin="16dp"
+        android:layout_marginStart="16dp"
         android:layout_marginTop="16dp"
-        android:layout_marginBottom="16dp"
-        app:layout_constraintBottom_toBottomOf="parent"
+        android:src="@drawable/ic_contact_picture"
         app:layout_constraintStart_toStartOf="parent"
-        app:layout_constraintTop_toTopOf="parent"
-        android:src="@drawable/ic_contact_picture" />
+        app:layout_constraintTop_toTopOf="parent" />

     <TextView
         android:id="@+id/name"
@@ -31,29 +27,27 @@
         android:layout_marginTop="16dp"
         android:ellipsize="end"
         android:gravity="center_vertical"
-        android:maxLines="1"
         android:textAppearance="@style/TextAppearance.MaterialComponents.Body1"
         android:textColor="?android:attr/textColorPrimary"
         app:layout_constrainedWidth="true"
-        app:layout_constraintBottom_toTopOf="@+id/address"
+        app:layout_constraintBottom_toTopOf="@+id/email"
         app:layout_constraintEnd_toStartOf="@+id/menu_add_contact"
         app:layout_constraintHorizontal_bias="0.0"
         app:layout_constraintHorizontal_chainStyle="packed"
         app:layout_constraintStart_toEndOf="@id/contact_picture"
         app:layout_constraintTop_toTopOf="parent"
         app:layout_constraintVertical_chainStyle="packed"
-        android:text="Alice" />
-
+        app:layout_goneMarginBottom="12dp"
+        tools:text="Alice" />

     <TextView
-        android:id="@+id/address"
+        android:id="@+id/email"
         android:layout_width="wrap_content"
         android:layout_height="wrap_content"
         android:layout_marginEnd="4dp"
         android:layout_marginBottom="16dp"
         android:ellipsize="end"
         android:gravity="center_vertical"
-        android:maxLines="1"
         android:textAppearance="@style/TextAppearance.MaterialComponents.Body2"
         android:textColor="?android:attr/textColorSecondary"
         app:layout_constrainedWidth="true"
@@ -63,17 +57,17 @@
         app:layout_constraintHorizontal_chainStyle="packed"
         app:layout_constraintStart_toStartOf="@+id/name"
         app:layout_constraintTop_toBottomOf="@+id/name"
-        android:text="alice@domain.example" />
-
+        tools:text="alice@domain.example" />

     <ImageView
         android:id="@+id/menu_add_contact"
         android:layout_width="48dp"
-        android:layout_height="48dp"
+        android:layout_height="72dp"
         android:background="?attr/controlBackground"
-        android:paddingHorizontal="12dp"
         android:clickable="true"
+        android:contentDescription="@string/action_add_to_contacts"
         android:focusable="true"
+        android:paddingHorizontal="12dp"
         app:layout_constraintEnd_toStartOf="@+id/menu_compose"
         app:layout_constraintTop_toTopOf="@+id/menu_compose"
         app:srcCompat="?attr/messageDetailsAddContactIcon" />
@@ -81,29 +75,30 @@
     <ImageView
         android:id="@+id/menu_compose"
         android:layout_width="48dp"
-        android:layout_height="48dp"
+        android:layout_height="72dp"
         android:background="?attr/controlBackground"
         android:clickable="true"
+        android:contentDescription="@string/compose_action"
         android:focusable="true"
+        android:paddingHorizontal="12dp"
         app:layout_constraintEnd_toStartOf="@id/menu_overflow"
         app:layout_constraintTop_toTopOf="@+id/menu_overflow"
-        android:paddingHorizontal="12dp"
         app:srcCompat="@drawable/ic_envelope" />

     <ImageView
         android:id="@+id/menu_overflow"
         android:layout_width="40dp"
-        android:layout_height="48dp"
-        android:layout_marginTop="16dp"
-        android:layout_marginBottom="16dp"
+        android:layout_height="72dp"
         android:background="?attr/controlBackground"
         android:clickable="true"
+        android:contentDescription="@string/abc_action_menu_overflow_description"
         android:focusable="true"
         android:paddingStart="6dp"
         android:paddingEnd="10dp"
         app:layout_constraintBottom_toBottomOf="parent"
         app:layout_constraintEnd_toEndOf="parent"
         app:layout_constraintTop_toTopOf="parent"
+        app:layout_constraintVertical_bias="0.0"
         app:srcCompat="@drawable/dots_vertical" />

 </androidx.constraintlayout.widget.ConstraintLayout>
diff --git a/app/ui/legacy/src/main/res/layout/message_details_section_header_item.xml b/app/ui/legacy/src/main/res/layout/message_details_section_header_item.xml
new file mode 100644
index 00000000000..9e3c855a8db
--- /dev/null
+++ b/app/ui/legacy/src/main/res/layout/message_details_section_header_item.xml
@@ -0,0 +1,28 @@
+<?xml version="1.0" encoding="utf-8"?>
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:tools="http://schemas.android.com/tools"
+    android:layout_width="match_parent"
+    android:layout_height="wrap_content"
+    android:layout_marginTop="8dp"
+    android:focusable="false"
+    android:orientation="horizontal">
+
+    <TextView
+        android:id="@+id/title"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:layout_marginStart="16dp"
+        android:textAppearance="?attr/textAppearanceOverline"
+        tools:text="To" />
+
+    <TextView
+        android:id="@+id/extra"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:layout_marginStart="4dp"
+        android:layout_marginEnd="16dp"
+        android:textAppearance="?attr/textAppearanceOverline"
+        android:textColor="?android:attr/textColorSecondary"
+        tools:text="3" />
+
+</LinearLayout>
diff --git a/app/ui/legacy/src/main/res/menu/participant_overflow_menu.xml b/app/ui/legacy/src/main/res/menu/participant_overflow_menu.xml
new file mode 100644
index 00000000000..b79ab3cc82f
--- /dev/null
+++ b/app/ui/legacy/src/main/res/menu/participant_overflow_menu.xml
@@ -0,0 +1,9 @@
+<?xml version="1.0" encoding="utf-8"?>
+<menu xmlns:android="http://schemas.android.com/apk/res/android">
+    <item
+        android:id="@+id/copy_email_address"
+        android:title="@string/action_copy_email_address" />
+    <item
+        android:id="@+id/copy_name_and_email_address"
+        android:title="@string/action_copy_name_and_email_address" />
+</menu>
diff --git a/app/ui/legacy/src/main/res/values/attrs.xml b/app/ui/legacy/src/main/res/values/attrs.xml
index b98c894c9d0..001cd401c3f 100644
--- a/app/ui/legacy/src/main/res/values/attrs.xml
+++ b/app/ui/legacy/src/main/res/values/attrs.xml
@@ -110,6 +110,7 @@
         <attr name="messageStarColor" format="color"/>
         <attr name="messageViewBackgroundColor" format="reference|color"/>
         <attr name="messageDetailsAddContactIcon" format="reference"/>
+        <attr name="messageDetailsDividerColor" format="reference|color"/>
         <attr name="attachmentCardBackground" format="reference|color"/>
         <attr name="composerBackgroundColor" format="color"/>
         <attr name="contactPictureFallbackDefaultBackgroundColor" format="reference|color"/>
diff --git a/app/ui/legacy/src/main/res/values/message_details_ids.xml b/app/ui/legacy/src/main/res/values/message_details_ids.xml
new file mode 100644
index 00000000000..a87383ded33
--- /dev/null
+++ b/app/ui/legacy/src/main/res/values/message_details_ids.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+    <item type="id" name="message_details_date"/>
+    <item type="id" name="message_details_section_header"/>
+    <item type="id" name="message_details_participant"/>
+    <item type="id" name="message_details_divider"/>
+</resources>
diff --git a/app/ui/legacy/src/main/res/values/strings.xml b/app/ui/legacy/src/main/res/values/strings.xml
index a00a8e8dd3d..5b3476a1f67 100644
--- a/app/ui/legacy/src/main/res/values/strings.xml
+++ b/app/ui/legacy/src/main/res/values/strings.xml
@@ -1307,4 +1307,36 @@ You can keep this message and use it as a backup for your secret key. If you wan
     <string name="message_list_error_title">Error</string>
     <!-- Displayed instead of the message list when a folder couldn't be found, e.g. due to an outdated home screen shortcut. -->
     <string name="message_list_error_folder_not_found">Folder not found</string>
+
+    <!-- Title of the toolbar that is displayed when the message details bottom sheet has been expanded -->
+    <string name="message_details_toolbar_title">Message details</string>
+    <!-- Displayed when an email is missing the 'Date' header field and no date can be displayed in the message details bottom sheet. -->
+    <string name="message_details_missing_date">Missing \'Date\' header</string>
+    <!-- Title of the "From" section in the message details bottom sheet. This should probably be the same as the string 'recipient_from'. -->
+    <string name="message_details_from_section_title">From</string>
+    <!-- Title of the "Sender" section in the message details bottom sheet. This refers to the 'Sender' header. Be careful with translations. -->
+    <string name="message_details_sender_section_title">Sender</string>
+    <!-- Title of the "Reply to" section in the message details bottom sheet. This should probably be the same as the string 'reply_to_label'. -->
+    <string name="message_details_replyto_section_title">Reply to</string>
+    <!-- Title of the "To" section in the message details bottom sheet. This should probably be the same as the string 'recipient_to'. -->
+    <string name="message_details_to_section_title">To</string>
+    <!-- Title of the "Cc" section in the message details bottom sheet. This should probably be the same as the string 'recipient_cc'. -->
+    <string name="message_details_cc_section_title">Cc</string>
+    <!-- Title of the "Bcc" section in the message details bottom sheet. This should probably be the same as the string 'recipient_bcc'. -->
+    <string name="message_details_bcc_section_title">Bcc</string>
+
+    <!-- Displayed when loading the message details has failed -->
+    <string name="message_details_loading_error">An error occurred while loading the message details.</string>
+
+    <!-- Name of the action to add a person to contacts. Usually displayed in a menu or in a tooltip when long-pressing the associated icon. -->
+    <string name="action_add_to_contacts">Add to contacts</string>
+    <!-- Name of the action to copy an email address to the clipboard. Usually displayed in a menu or in a tooltip when long-pressing the associated icon. -->
+    <string name="action_copy_email_address">Copy email address</string>
+    <!-- Name of the action to copy a name and email address to the clipboard. Usually displayed in a menu or in a tooltip when long-pressing the associated icon. -->
+    <string name="action_copy_name_and_email_address">Copy name and email address</string>
+
+    <!-- A user visible label for the email address copied to the clipboard -->
+    <string name="clipboard_label_email_address">Email address</string>
+    <!-- A user visible label for the name and email address copied to the clipboard -->
+    <string name="clipboard_label_name_and_email_address">Name and email address</string>
 </resources>
diff --git a/app/ui/legacy/src/main/res/values/themes.xml b/app/ui/legacy/src/main/res/values/themes.xml
index 5e40871284c..2a64b98cc8a 100644
--- a/app/ui/legacy/src/main/res/values/themes.xml
+++ b/app/ui/legacy/src/main/res/values/themes.xml
@@ -134,6 +134,7 @@
         <item name="messageStarColor">#fbbc04</item>
         <item name="messageViewBackgroundColor">#ffffffff</item>
         <item name="messageDetailsAddContactIcon">@drawable/ic_person_add</item>
+        <item name="messageDetailsDividerColor">#ffcccccc</item>
         <item name="attachmentCardBackground">#e8e8e8</item>
         <item name="contactPictureFallbackDefaultBackgroundColor">#ffababab</item>
         <item name="contactPictureFallbackBackgroundColors">@array/contact_picture_fallback_background_colors_light</item>
@@ -296,6 +297,7 @@
         <item name="messageStarColor">#fdd663</item>
         <item name="messageViewBackgroundColor">#000000</item>
         <item name="messageDetailsAddContactIcon">@drawable/ic_person_add</item>
+        <item name="messageDetailsDividerColor">#ff555555</item>
         <item name="attachmentCardBackground">#313131</item>
         <item name="contactTokenBackgroundColor">#313131</item>
         <item name="contactPictureFallbackDefaultBackgroundColor">#ff606060</item>
diff --git a/backend/demo/src/main/resources/contents.json b/backend/demo/src/main/resources/contents.json
index 87879b8cc2f..03a7c033a17 100644
--- a/backend/demo/src/main/resources/contents.json
+++ b/backend/demo/src/main/resources/contents.json
@@ -2,7 +2,7 @@
   "inbox": {
     "name": "Inbox",
     "type": "INBOX",
-    "messageServerIds": ["intro"]
+    "messageServerIds": ["intro", "many_recipients"]
   },
   "trash": {
     "name": "Trash",
diff --git a/backend/demo/src/main/resources/inbox/many_recipients.eml b/backend/demo/src/main/resources/inbox/many_recipients.eml
new file mode 100644
index 00000000000..a0bd8317d89
--- /dev/null
+++ b/backend/demo/src/main/resources/inbox/many_recipients.eml
@@ -0,0 +1,42 @@
+MIME-Version: 1.0
+From: "Alice" <from1@k9mail.example>, "Bob" <from2@k9mail.example>
+Sender: "Bernd" <sender@k9mail.example>
+Reply-To: <reply-to@k9mail.example>
+Date: Mon, 23 Jan 2023 12:00:00 +0100
+Message-ID: <inbox-2@k9mail.example>
+Subject: Message details demo
+To: "User 1" <to1@k9mail.example>,
+ "User 2" <to2@k9mail.example>,
+ "User 3" <to3@k9mail.example>,
+ "User 4" <to4@k9mail.example>,
+ "User 5" <to5@k9mail.example>,
+ "User 6" <to6@k9mail.example>,
+ "User 7" <to7@k9mail.example>,
+ "User 8" <to8@k9mail.example>,
+ "User 9" <to9@k9mail.example>,
+ "User 10" <to10@k9mail.example>,
+ "User 11" <to11@k9mail.example>,
+ "User 12" <to12@k9mail.example>,
+ "User 13" <to13@k9mail.example>,
+ "User 14" <to14@k9mail.example>,
+ "User 15" <to15@k9mail.example>,
+ "User 16" <to16@k9mail.example>,
+ "User 17" <to17@k9mail.example>,
+ "User 18" <to18@k10mail.example>,
+ "User 19" <to19@k11mail.example>,
+ "User 20" <to20@k12mail.example>
+Cc: "Copy 1" <cc1@k9mail.example>,
+ "Copy 2" <cc2@k9mail.example>,
+ "Copy 3" <cc3@k9mail.example>
+Bcc: "Blind 1" <bcc1@k9mail.example>,
+ "Blind 2" <bcc2@k9mail.example>,
+ "Blind 3" <bcc3@k9mail.example>
+Content-Type: text/plain; charset=UTF-8
+
+This message contains…
+- multiple addresses in the From: header
+- a Sender: header
+- a Reply-To: header
+- multiple addresses in the To: header
+- multiple addresses in the Cc: header
+- multiple addresses in the Bcc: header
diff --git a/mail/protocols/imap/src/main/java/com/fsck/k9/mail/store/imap/RealImapFolder.kt b/mail/protocols/imap/src/main/java/com/fsck/k9/mail/store/imap/RealImapFolder.kt
index 58a888ddd35..a74271c2ef2 100644
--- a/mail/protocols/imap/src/main/java/com/fsck/k9/mail/store/imap/RealImapFolder.kt
+++ b/mail/protocols/imap/src/main/java/com/fsck/k9/mail/store/imap/RealImapFolder.kt
@@ -538,7 +538,7 @@ internal class RealImapFolder(
             fetchFields.add("RFC822.SIZE")
             fetchFields.add(
                 "BODY.PEEK[HEADER.FIELDS (date subject from content-type to cc " +
-                    "reply-to message-id references in-reply-to list-unsubscribe " +
+                    "reply-to message-id references in-reply-to list-unsubscribe sender " +
                     K9MailLib.IDENTITY_HEADER + " " + K9MailLib.CHAT_HEADER + ")]"
             )
         }
diff --git a/ui-utils/ToolbarBottomSheet/src/main/java/app/k9mail/ui/utils/bottomsheet/ToolbarBottomSheetDialog.kt b/ui-utils/ToolbarBottomSheet/src/main/java/app/k9mail/ui/utils/bottomsheet/ToolbarBottomSheetDialog.kt
index 8255a8acb6c..159f74ce477 100644
--- a/ui-utils/ToolbarBottomSheet/src/main/java/app/k9mail/ui/utils/bottomsheet/ToolbarBottomSheetDialog.kt
+++ b/ui-utils/ToolbarBottomSheet/src/main/java/app/k9mail/ui/utils/bottomsheet/ToolbarBottomSheetDialog.kt
@@ -34,7 +34,6 @@ import androidx.annotation.StyleRes
 import androidx.appcompat.app.AppCompatDialog
 import androidx.appcompat.widget.Toolbar
 import androidx.coordinatorlayout.widget.CoordinatorLayout
-import androidx.core.graphics.Insets
 import androidx.core.view.AccessibilityDelegateCompat
 import androidx.core.view.ViewCompat
 import androidx.core.view.WindowCompat
@@ -191,6 +190,19 @@ class ToolbarBottomSheetDialog internal constructor(context: Context, @StyleRes
             container.doOnLayout {
                 behavior.peekHeight = container.height / 2
             }
+
+            bottomSheet.doOnLayout {
+                // Don't draw the toolbar underneath the status bar if the bottom sheet doesn't cover the whole screen
+                // anyway.
+                if (bottomSheet.width < container.width) {
+                    container.fitsSystemWindows = true
+                    coordinator?.fitsSystemWindows = true
+                    setToolbarVisibilityCallback(topInset = 0)
+                } else {
+                    container.fitsSystemWindows = false
+                    coordinator?.fitsSystemWindows = false
+                }
+            }
         }

         return container
@@ -200,27 +212,15 @@ class ToolbarBottomSheetDialog internal constructor(context: Context, @StyleRes
     private fun wrapInBottomSheet(view: View): View {
         ensureContainerAndBehavior()

-        val window = checkNotNull(window)
         val container = checkNotNull(container)
         val coordinator = checkNotNull(coordinator)
         val bottomSheet = checkNotNull(bottomSheet)
-        val toolbar = checkNotNull(toolbar)

         ViewCompat.setOnApplyWindowInsetsListener(bottomSheet) { _, windowInsets ->
-            val behavior = checkNotNull(internalBehavior)
-
             val insets = windowInsets.getInsets(WindowInsetsCompat.Type.systemBars())
+            val topInset = insets.top

-            toolbar.setPadding(0, insets.top, 0, 0)
-
-            toolbarVisibilityCallback?.let { oldCallback ->
-                behavior.removeBottomSheetCallback(oldCallback)
-            }
-
-            val windowInsetsController = WindowCompat.getInsetsController(window, bottomSheet)
-            val newCallback = ToolbarVisibilityCallback(windowInsetsController, toolbar, insets, behavior)
-            behavior.addBottomSheetCallback(newCallback)
-            this.toolbarVisibilityCallback = newCallback
+            setToolbarVisibilityCallback(topInset)

             WindowInsetsCompat.CONSUMED
         }
@@ -268,6 +268,23 @@ class ToolbarBottomSheetDialog internal constructor(context: Context, @StyleRes
         return container
     }

+    private fun setToolbarVisibilityCallback(topInset: Int) {
+        val window = checkNotNull(window)
+        val bottomSheet = checkNotNull(bottomSheet)
+        val toolbar = checkNotNull(toolbar)
+        val behavior = checkNotNull(internalBehavior)
+
+        toolbarVisibilityCallback?.let { oldCallback ->
+            behavior.removeBottomSheetCallback(oldCallback)
+        }
+
+        val windowInsetsController = WindowCompat.getInsetsController(window, bottomSheet)
+        val newCallback = ToolbarVisibilityCallback(windowInsetsController, toolbar, topInset, behavior)
+        behavior.addBottomSheetCallback(newCallback)
+
+        this.toolbarVisibilityCallback = newCallback
+    }
+
     fun removeDefaultCallback() {
         checkNotNull(internalBehavior).removeBottomSheetCallback(cancelDialogCallback)
     }
@@ -285,7 +302,7 @@ class ToolbarBottomSheetDialog internal constructor(context: Context, @StyleRes
     private class ToolbarVisibilityCallback(
         private val windowInsetsController: WindowInsetsControllerCompat,
         private val toolbar: Toolbar,
-        private val insets: Insets,
+        private val topInset: Int,
         private val behavior: BottomSheetBehavior<FrameLayout>
     ) : LayoutAwareBottomSheetCallback() {

@@ -335,14 +352,14 @@ class ToolbarBottomSheetDialog internal constructor(context: Context, @StyleRes
             val top = bottomSheet.top

             val collapsedOffset = (bottomSheet.parent as View).height / 2
-            if (top >= collapsedOffset) {
+            if (top >= collapsedOffset || behavior.expandedOffset > 0) {
                 toolbar.isInvisible = true
                 bottomSheet.setPadding(0, 0, 0, 0)
                 return
             }

             val toolbarHeight = toolbar.height - toolbar.paddingTop
-            val toolbarHeightAndInset = toolbarHeight + insets.top
+            val toolbarHeightAndInset = toolbarHeight + topInset
             val expandedPercentage =
                 ((collapsedOffset - top).toFloat() / (collapsedOffset - behavior.expandedOffset)).coerceAtMost(1f)

@@ -350,7 +367,12 @@ class ToolbarBottomSheetDialog internal constructor(context: Context, @StyleRes
             val paddingTop = (toolbarHeightAndInset * expandedPercentage).toInt().coerceAtLeast(0)
             bottomSheet.setPadding(0, paddingTop, 0, 0)

-            if (paddingTop > toolbarHeight) {
+            // Start showing the toolbar when the bottom sheet is a toolbar height away from the top of the screen.
+            // This value was chosen rather arbitrarily because it looked nice enough.
+            val toolbarPercentage =
+                ((toolbarHeight - top).toFloat() / (toolbarHeight - behavior.expandedOffset)).coerceAtLeast(0f)
+
+            if (toolbarPercentage > 0) {
                 toolbar.isVisible = true

                 // Set the toolbar's top padding so the toolbar covers the bottom sheet's whole top padding
@@ -360,18 +382,14 @@ class ToolbarBottomSheetDialog internal constructor(context: Context, @StyleRes
                 // Translate the toolbar view so it is drawn on top of the bottom sheet's top padding
                 toolbar.translationY = -paddingTop.toFloat()

-                // Start fading in the toolbar when the bottom sheet is a toolbar height away from the top of the screen.
-                // This value was chosen rather arbitrarily because it looked nice.
-                val alphaPercentage =
-                    ((toolbarHeight - top).toFloat() / (toolbarHeight - behavior.expandedOffset)).coerceAtLeast(0f)
-                toolbar.alpha = alphaPercentage
+                toolbar.alpha = toolbarPercentage
             } else {
                 toolbar.isInvisible = true
             }
         }

         private fun setStatusBarColor(bottomSheet: View) {
-            val toolbarLightThreshold = insets.top / 2
+            val toolbarLightThreshold = topInset / 2
             if (bottomSheet.top < toolbarLightThreshold) {
                 windowInsetsController.isAppearanceLightStatusBars = lightToolbar
             } else if (bottomSheet.top != 0) {

diff --git a/app/storage/src/test/java/com/fsck/k9/storage/messages/RetrieveMessageOperationsTest.kt b/app/storage/src/test/java/com/fsck/k9/storage/messages/RetrieveMessageOperationsTest.kt
index 09dccb31d70..09246c3cb30 100644
--- a/app/storage/src/test/java/com/fsck/k9/storage/messages/RetrieveMessageOperationsTest.kt
+++ b/app/storage/src/test/java/com/fsck/k9/storage/messages/RetrieveMessageOperationsTest.kt
@@ -170,6 +170,34 @@ class RetrieveMessageOperationsTest : RobolectricTest() {
         )
     }

+    @Test
+    fun `get some headers`() {
+        val messagePartId = sqliteDatabase.createMessagePart(
+            header = """
+                From: <alice@domain.example>
+                To: Bob <bob@domain.example>
+                Date: Thu, 01 Apr 2021 01:23:45 +0200
+                Subject: Test
+                Message-Id: <20210401012345.123456789A@domain.example>
+            """.trimIndent().crlf()
+        )
+        sqliteDatabase.createMessage(folderId = 1, uid = "uid1", messagePartId = messagePartId)
+
+        val headers = retrieveMessageOperations.getHeaders(
+            folderId = 1,
+            messageServerId = "uid1",
+            headerNames = setOf("from", "to", "message-id")
+        )
+
+        assertThat(headers).isEqualTo(
+            listOf(
+                Header("From", "<alice@domain.example>"),
+                Header("To", "Bob <bob@domain.example>"),
+                Header("Message-Id", "<20210401012345.123456789A@domain.example>")
+            )
+        )
+    }
+
     @Test
     fun `get oldest message date`() {
         sqliteDatabase.createMessage(folderId = 1, date = 42)
diff --git a/mail/protocols/imap/src/test/java/com/fsck/k9/mail/store/imap/RealImapFolderTest.kt b/mail/protocols/imap/src/test/java/com/fsck/k9/mail/store/imap/RealImapFolderTest.kt
index 0ff0ada4a95..b1beda25bdc 100644
--- a/mail/protocols/imap/src/test/java/com/fsck/k9/mail/store/imap/RealImapFolderTest.kt
+++ b/mail/protocols/imap/src/test/java/com/fsck/k9/mail/store/imap/RealImapFolderTest.kt
@@ -698,7 +698,7 @@ class RealImapFolderTest {
         verify(imapConnection).sendCommand(
             "UID FETCH 1 (UID INTERNALDATE RFC822.SIZE BODY.PEEK[HEADER.FIELDS " +
                 "(date subject from content-type to cc reply-to message-id references in-reply-to list-unsubscribe " +
-                "X-K9mail-Identity Chat-Version)])",
+                "sender X-K9mail-Identity Chat-Version)])",
             false
         )
     }


Keep `<base>` element

Fixes #6587

Relative link and `<base>` tag

Relative link and `<base>` tag
### Checklist

- [X] I have used the search function to see if someone else has already submitted the same bug report.
- [X] I will describe the problem with as much detail as possible.

### App version

6.502

### Where did you get the app from?

None

### Android version

doesn't matter

### Device model

_No response_

### Steps to reproduce

1. Open a message containing a `<base>` element and a relative link
2. Tap the link

```email
MIME-Version: 1.0
From: "cketti" <cketti@k9mail.example>
Date: Tue, 17 Jan 2023 16:00:00 +0100
Message-ID: <inbox-2@k9mail.example>
Subject: Relative link
To: User <user@k9mail.example>
Content-Type: text/html; charset=UTF-8

<html>
  <head>
    <base href="https://k9mail.app/">
  </head>
  <body>
    <a href="/2022/06/13/K-9-Mail-and-Thunderbird">Link</a>
  </body>
</html>
```

### Expected behavior

K-9 Mail resolves the relative URL using the base URL and hands the proper URL to an external app for viewing.

### Actual behavior

The URL can't be resolved and the URI `about:blank#blocked` is used when launching an external app.

Long-pressing the link also doesn't work.

### Logs

_No response_

./gradlew :app:html-cleaner:test --tests "app.k9mail.html.cleaner.HtmlSanitizerTest"

diff --git a/app/html-cleaner/src/main/java/app/k9mail/html/cleaner/HeadCleaner.kt b/app/html-cleaner/src/main/java/app/k9mail/html/cleaner/HeadCleaner.kt
index d9ac70f810c..1485291428d 100644
--- a/app/html-cleaner/src/main/java/app/k9mail/html/cleaner/HeadCleaner.kt
+++ b/app/html-cleaner/src/main/java/app/k9mail/html/cleaner/HeadCleaner.kt
@@ -9,7 +9,7 @@ import org.jsoup.parser.Tag
 import org.jsoup.select.NodeTraversor
 import org.jsoup.select.NodeVisitor

-private val ALLOWED_TAGS = listOf("style", "meta")
+private val ALLOWED_TAGS = listOf("style", "meta", "base")

 internal class HeadCleaner {
     fun clean(dirtyDocument: Document, cleanedDocument: Document) {

diff --git a/app/html-cleaner/src/test/java/app/k9mail/html/cleaner/HtmlSanitizerTest.kt b/app/html-cleaner/src/test/java/app/k9mail/html/cleaner/HtmlSanitizerTest.kt
index d60ae8b3812..90250000671 100644
--- a/app/html-cleaner/src/test/java/app/k9mail/html/cleaner/HtmlSanitizerTest.kt
+++ b/app/html-cleaner/src/test/java/app/k9mail/html/cleaner/HtmlSanitizerTest.kt
@@ -465,6 +465,25 @@ class HtmlSanitizerTest {
         assertTagsNotStripped("var")
     }

+    @Test
+    fun `should keep 'base' element`() {
+        val html =
+            """
+
+
+                <base href="https://domain.example/">
+
+
+                Link
+
+
+            """.compactHtml()
+
+        val result = htmlSanitizer.sanitize(html)
+
+        assertThat(result.toCompactString()).isEqualTo(html)
+    }
+
     private fun assertTagsNotStripped(element: String) {
         val html = """<$element>some text</$element>"""

@@ -491,4 +510,6 @@ class HtmlSanitizerTest {
     }

     private fun String.trimLineBreaks() = replace("\n", "")
+
+    private fun String.compactHtml() = lines().joinToString(separator = "") { it.trim() }
 }


Change `WorkManager` initialization

This should fix #6553.

Crash because WorkManager isn't initialized properly
Stack trace retrieved via Google Play Console:

```text
Exception java.lang.RuntimeException: Unable to create service androidx.work.impl.background.systemjob.SystemJobService: java.lang.IllegalStateException: WorkManager needs to be initialized via a ContentProvider#onCreate() or an Application#onCreate().
  at android.app.ActivityThread.handleCreateService (ActivityThread.java:4355)
  at android.app.ActivityThread.access$1800 (ActivityThread.java:261)
  at android.app.ActivityThread$H.handleMessage (ActivityThread.java:2044)
  at android.os.Handler.dispatchMessage (Handler.java:106)
  at android.os.Looper.loop (Looper.java:236)
  at android.app.ActivityThread.main (ActivityThread.java:8051)
  at java.lang.reflect.Method.invoke
  at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run (RuntimeInit.java:620)
  at com.android.internal.os.ZygoteInit.main (ZygoteInit.java:1011)
Caused by java.lang.IllegalStateException: WorkManager needs to be initialized via a ContentProvider#onCreate() or an Application#onCreate().
  at androidx.work.impl.background.systemjob.SystemJobService.onCreate (SystemJobService.java:73)
  at android.app.ActivityThread.handleCreateService (ActivityThread.java:4343)
```

./gradlew :app:k9mail:testDebugUnitTest --tests "com.fsck.k9.DependencyInjectionTest"

diff --git a/app/core/src/main/java/com/fsck/k9/job/K9WorkerFactory.kt b/app/core/src/main/java/com/fsck/k9/job/K9WorkerFactory.kt
index adcb9258812..71af4cbf3da 100644
--- a/app/core/src/main/java/com/fsck/k9/job/K9WorkerFactory.kt
+++ b/app/core/src/main/java/com/fsck/k9/job/K9WorkerFactory.kt
@@ -4,23 +4,16 @@ import android.content.Context
 import androidx.work.ListenableWorker
 import androidx.work.WorkerFactory
 import androidx.work.WorkerParameters
-import com.fsck.k9.Preferences
-import com.fsck.k9.controller.MessagingController
+import org.koin.core.parameter.parametersOf
+import org.koin.java.KoinJavaComponent.getKoin

-class K9WorkerFactory(
-    private val messagingController: MessagingController,
-    private val preferences: Preferences
-) : WorkerFactory() {
+class K9WorkerFactory : WorkerFactory() {
     override fun createWorker(
         appContext: Context,
         workerClassName: String,
         workerParameters: WorkerParameters
-    ): ListenableWorker? {
-        return when (workerClassName) {
-            MailSyncWorker::class.java.canonicalName -> {
-                MailSyncWorker(messagingController, preferences, appContext, workerParameters)
-            }
-            else -> null
-        }
+    ): ListenableWorker {
+        val workerClass = Class.forName(workerClassName).kotlin
+        return getKoin().get(workerClass) { parametersOf(workerParameters) }
     }
 }
diff --git a/app/core/src/main/java/com/fsck/k9/job/KoinModule.kt b/app/core/src/main/java/com/fsck/k9/job/KoinModule.kt
index 4dd6f0e8fa3..8158ae494b4 100644
--- a/app/core/src/main/java/com/fsck/k9/job/KoinModule.kt
+++ b/app/core/src/main/java/com/fsck/k9/job/KoinModule.kt
@@ -1,12 +1,18 @@
 package com.fsck.k9.job

+import android.content.Context
+import androidx.work.WorkManager
 import androidx.work.WorkerFactory
+import androidx.work.WorkerParameters
 import org.koin.dsl.module

 val jobModule = module {
-    single { WorkManagerProvider(get(), get()) }
-    single<WorkerFactory> { K9WorkerFactory(get(), get()) }
-    single { get<WorkManagerProvider>().getWorkManager() }
-    single { K9JobManager(get(), get(), get()) }
+    single { WorkManagerConfigurationProvider(workerFactory = get()) }
+    single<WorkerFactory> { K9WorkerFactory() }
+    single { WorkManager.getInstance(get<Context>()) }
+    single { K9JobManager(workManager = get(), preferences = get(), mailSyncWorkerManager = get()) }
     factory { MailSyncWorkerManager(workManager = get(), clock = get()) }
+    factory { (parameters: WorkerParameters) ->
+        MailSyncWorker(messagingController = get(), preferences = get(), context = get(), parameters)
+    }
 }
diff --git a/app/core/src/main/java/com/fsck/k9/job/WorkManagerConfigurationProvider.kt b/app/core/src/main/java/com/fsck/k9/job/WorkManagerConfigurationProvider.kt
new file mode 100644
index 00000000000..54d973fee18
--- /dev/null
+++ b/app/core/src/main/java/com/fsck/k9/job/WorkManagerConfigurationProvider.kt
@@ -0,0 +1,12 @@
+package com.fsck.k9.job
+
+import androidx.work.Configuration
+import androidx.work.WorkerFactory
+
+class WorkManagerConfigurationProvider(private val workerFactory: WorkerFactory) {
+    fun getConfiguration(): Configuration {
+        return Configuration.Builder()
+            .setWorkerFactory(workerFactory)
+            .build()
+    }
+}
diff --git a/app/core/src/main/java/com/fsck/k9/job/WorkManagerProvider.kt b/app/core/src/main/java/com/fsck/k9/job/WorkManagerProvider.kt
deleted file mode 100644
index f31fbaaf7b2..00000000000
--- a/app/core/src/main/java/com/fsck/k9/job/WorkManagerProvider.kt
+++ /dev/null
@@ -1,18 +0,0 @@
-package com.fsck.k9.job
-
-import android.content.Context
-import androidx.work.Configuration
-import androidx.work.WorkManager
-import androidx.work.WorkerFactory
-
-class WorkManagerProvider(private val context: Context, private val workerFactory: WorkerFactory) {
-    fun getWorkManager(): WorkManager {
-        val configuration = Configuration.Builder()
-            .setWorkerFactory(workerFactory)
-            .build()
-
-        WorkManager.initialize(context, configuration)
-
-        return WorkManager.getInstance(context)
-    }
-}
diff --git a/app/k9mail/build.gradle b/app/k9mail/build.gradle
index cbd49fe57f3..2c55af72ea9 100644
--- a/app/k9mail/build.gradle
+++ b/app/k9mail/build.gradle
@@ -18,6 +18,7 @@ dependencies {

     implementation "androidx.appcompat:appcompat:${versions.androidxAppCompat}"
     implementation "androidx.core:core-ktx:${versions.androidxCore}"
+    implementation "androidx.work:work-runtime-ktx:${versions.androidxWorkManager}"
     implementation "com.takisoft.preferencex:preferencex:${versions.preferencesFix}"
     implementation "com.jakewharton.timber:timber:${versions.timber}"
     implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:${versions.kotlinCoroutines}"
diff --git a/app/k9mail/src/main/java/com/fsck/k9/App.kt b/app/k9mail/src/main/java/com/fsck/k9/App.kt
index 00821045afc..7f87dcfe2bc 100644
--- a/app/k9mail/src/main/java/com/fsck/k9/App.kt
+++ b/app/k9mail/src/main/java/com/fsck/k9/App.kt
@@ -7,6 +7,7 @@ import app.k9mail.ui.widget.list.MessageListWidgetManager
 import com.fsck.k9.activity.LauncherShortcuts
 import com.fsck.k9.activity.MessageCompose
 import com.fsck.k9.controller.MessagingController
+import com.fsck.k9.job.WorkManagerConfigurationProvider
 import com.fsck.k9.notification.NotificationChannelManager
 import com.fsck.k9.provider.UnreadWidgetProvider
 import com.fsck.k9.ui.base.AppLanguageManager
@@ -24,14 +25,17 @@ import kotlinx.coroutines.flow.onEach
 import kotlinx.coroutines.plus
 import org.koin.android.ext.android.inject
 import timber.log.Timber
+import androidx.work.Configuration as WorkManagerConfiguration

-class App : Application() {
+class App : Application(), WorkManagerConfiguration.Provider {
     private val messagingController: MessagingController by inject()
     private val messagingListenerProvider: MessagingListenerProvider by inject()
     private val themeManager: ThemeManager by inject()
     private val appLanguageManager: AppLanguageManager by inject()
     private val notificationChannelManager: NotificationChannelManager by inject()
     private val messageListWidgetManager: MessageListWidgetManager by inject()
+    private val workManagerConfigurationProvider: WorkManagerConfigurationProvider by inject()
+
     private val appCoroutineScope: CoroutineScope = GlobalScope + Dispatchers.Main
     private var appLanguageManagerInitialized = false

@@ -124,6 +128,10 @@ class App : Application() {
             .launchIn(appCoroutineScope)
     }

+    override fun getWorkManagerConfiguration(): WorkManagerConfiguration {
+        return workManagerConfigurationProvider.getConfiguration()
+    }
+
     companion object {
         val appConfig = AppConfig(
             componentsToDisable = listOf(

diff --git a/app/core/src/test/java/com/fsck/k9/TestApp.kt b/app/core/src/test/java/com/fsck/k9/TestApp.kt
index b365bd4ddc5..7f6f0f88677 100644
--- a/app/core/src/test/java/com/fsck/k9/TestApp.kt
+++ b/app/core/src/test/java/com/fsck/k9/TestApp.kt
@@ -1,6 +1,7 @@
 package com.fsck.k9

 import android.app.Application
+import androidx.work.WorkManager
 import com.fsck.k9.backend.BackendManager
 import com.fsck.k9.controller.ControllerExtension
 import com.fsck.k9.crypto.EncryptionExtractor
@@ -36,4 +37,5 @@ val testModule = module {
     single { mock<NotificationActionCreator>() }
     single { mock<NotificationStrategy>() }
     single(named("controllerExtensions")) { emptyList<ControllerExtension>() }
+    single { mock<WorkManager>() }
 }
diff --git a/app/k9mail/src/test/java/com/fsck/k9/DependencyInjectionTest.kt b/app/k9mail/src/test/java/com/fsck/k9/DependencyInjectionTest.kt
index f9da2d72cae..09f3508db35 100644
--- a/app/k9mail/src/test/java/com/fsck/k9/DependencyInjectionTest.kt
+++ b/app/k9mail/src/test/java/com/fsck/k9/DependencyInjectionTest.kt
@@ -1,6 +1,8 @@
 package com.fsck.k9

 import androidx.lifecycle.LifecycleOwner
+import androidx.work.WorkerParameters
+import com.fsck.k9.job.MailSyncWorker
 import com.fsck.k9.ui.changelog.ChangeLogMode
 import com.fsck.k9.ui.changelog.ChangelogViewModel
 import com.fsck.k9.ui.endtoend.AutocryptKeyTransferActivity
@@ -41,6 +43,7 @@ class DependencyInjectionTest : AutoCloseKoinTest() {
             withParameter<FolderNameFormatter> { RuntimeEnvironment.getApplication() }
             withParameter<SizeFormatter> { RuntimeEnvironment.getApplication() }
             withParameter<ChangelogViewModel> { ChangeLogMode.CHANGE_LOG }
+            withParameter<MailSyncWorker> { mock<WorkerParameters>() }
         }
     }
 }