Repository: thunderbird/thunderbird-android (language: kotlin)

Issue:
Swipe left/right on message to perform action
### Expected behaviour

Swipe right on message in the message list may delete message (like in iOS and AquaMail for Android).
### Actual behaviour

Swipe do nothing. There is no such feature.
### Steps to reproduce
1. Just try to swipe on the message in message list. Nothing. Only context menu if you will hold your finger on message.
### Environment

K-9 Mail version: 5.0.10

Android version: 6.0.1

Account type (IMAP, POP3, WebDAV/Exchange): POP3



Discussion hints:
Maybe swipe left to archive, as well.

The Gmail app also has this feature; I find it very useful to quickly sort out messages I don't care about.
NO.
That is the stupidest thing that could possibly ever be added.
I don't need any email to be randomly deleted by an unintentional sideways slip.

And as it happens, that motion IS mapped to a function in k9mail -- SELECT.
The gmail app handles the occasional accidental swipes via an "undo". Both desires could be satisfied by making it an optional feature.
Having this configurable could be a good option
@lbdroid would something like this be fairly acceptable in terms of functionality? Being opt-in, it should not disrupt the current UX too much.

As a side note, i just quickly put this together, and I'm by no means an android developer myself, so it would need a thorough review before i consider making a pull request out of that.

@jszwedko thoughts?
To just add some concerns from my side:
 - Current code does not do select/deselect as it states around, it actually does toggle-select regardless of the swipe direction. Git history did not show anything on a quick scan.
 - There's no feedback for the actions taken, unlike some other apps like gmail, whcih means it's hard to be sure if you deleted or archived emails. Better feedback (eg transitions or bubble) could be helpful, but I feel that's a completely separate feature anyway.
 - Other actions could be useful (flagging? starring? mark as spam?)
 - I'm not completely happy with the naming/namespacing of some of the bits in my commit :)
Also relevant: #2807
This is implemented by AquaMail by displaying an icon of the action that will happen upon completing the swipe and an undo popup. It is extremely comfortable when you go for the inbox-0 strategy of archiving processed mail.

Do you accept bounties?
Hello
I stumbled on this thread via Google. 
I switched from iPhone to Android 2 weeks ago and tried several email apps. 
K-9 and aqua remained on my "desktop". I cannot delete aqua because it has this really efficient swiping feature...
I'd be glad if it was added to k-9, too! 
BTW. : some iPhone app (mail?) even featured three actions with swipe. But that was quite error prone for me. 
#Same here. It is on the default app on my Sony Xperia. I also was perplexed by the possibility of accidentally deleting emails at first but it turns out it doesn't happen and I use the function a lot.
Here is how it is configured in my app :
- Swipe left : sort/archive
- Swipe right : delete
- Long touch : select

It is really handy that way.
I would love that feature. Just moved from a Samsung to a Nokia phone und thus missing this standard of Samsung mail. 
+1. helps for fast cleanup of inbox/folders!
Really useful feature, make it configurable, pick a sensible default.
Who the heck is marking all supporting comments as spam or off-topic? Pls provide other means to vote if you don't want that in the bugtracker!
> Pls provide other means to vote

@ikcalB the comment reactions (👍) are precisely such an alternative.
Hi, a issue Ability to quickly delete messages from the message list: #623 has been closed and Design Overhaul moved this from To Do to Done, but the feature doesn't work. When I try to swipe a mail nothing happens. It's it normal?
I'm using k-9 v 5.6 on Android 9.0
Hi! All email apps I tested have this feature (Gmail, aquamail, BlueMail, maildroid, edison, fairEmail, outlook...)
Any news on k9? 
We're also looking forward to see this feature. But, as mentioned before, actions should be configurable anyway. It's impossible to predict which action the user would intuitively assume on swiping left right.
Is there any intention to add the SWIPE functionality? This is the fastest possibility to delete / archive messages. 
To be honest I went already multiple times away from K-9 because it is missing this possibility. I receive many mails per day where I just can swipe directly.
SWIPE is a base functionality on any smartphone. Everybody is used to it. In K-9 you first have to click and then you have to select the delete button. This is just not user friendly! 100 mails 200 klicks **vs.** 100 swipes. 

I like to keep my inbox "clean" so for me the use-case of "selecting" many messages and deleting all at once is just not a real use-case. 
Thanks for all your efforts.

Pull request: Add support for swipe actions to the message list screen
The default actions are currently:
- Swipe right: Toggle selection
- Swipe left: Mark as read/unread

https://user-images.githubusercontent.com/218061/194175023-d3a6f383-8130-4a02-9921-9aa48ee6ac6c.mp4


Closes #1426

Test command: ./gradlew :app:ui:legacy:testDebugUnitTest --tests "com.fsck.k9.ui.messagelist.MessageListAdapterTest"

Fix patch:
diff --git a/app/core/src/main/java/com/fsck/k9/K9.kt b/app/core/src/main/java/com/fsck/k9/K9.kt
index efcf8616e0a..d52f49dc84b 100644
--- a/app/core/src/main/java/com/fsck/k9/K9.kt
+++ b/app/core/src/main/java/com/fsck/k9/K9.kt
@@ -250,6 +250,12 @@ object K9 : EarlyInit {
     @JvmStatic
     var pgpSignOnlyDialogCounter: Int = 0
 
+    @JvmStatic
+    var swipeRightAction: SwipeAction = SwipeAction.ToggleSelection
+
+    @JvmStatic
+    var swipeLeftAction: SwipeAction = SwipeAction.ToggleRead
+
     val isQuietTime: Boolean
         get() {
             if (!isQuietTimeEnabled) {
@@ -358,6 +364,9 @@ object K9 : EarlyInit {
         pgpSignOnlyDialogCounter = storage.getInt("pgpSignOnlyDialogCounter", 0)
 
         k9Language = storage.getString("language", "")
+
+        swipeRightAction = storage.getEnum("swipeRightAction", SwipeAction.ToggleSelection)
+        swipeLeftAction = storage.getEnum("swipeLeftAction", SwipeAction.ToggleRead)
     }
 
     internal fun save(editor: StorageEditor) {
@@ -417,6 +426,9 @@ object K9 : EarlyInit {
         editor.putInt("pgpInlineDialogCounter", pgpInlineDialogCounter)
         editor.putInt("pgpSignOnlyDialogCounter", pgpSignOnlyDialogCounter)
 
+        editor.putEnum("swipeRightAction", swipeRightAction)
+        editor.putEnum("swipeLeftAction", swipeLeftAction)
+
         fontSizes.save(editor)
     }
 
diff --git a/app/core/src/main/java/com/fsck/k9/SwipeAction.kt b/app/core/src/main/java/com/fsck/k9/SwipeAction.kt
new file mode 100644
index 00000000000..5d4fc943415
--- /dev/null
+++ b/app/core/src/main/java/com/fsck/k9/SwipeAction.kt
@@ -0,0 +1,12 @@
+package com.fsck.k9
+
+enum class SwipeAction {
+    None,
+    ToggleSelection,
+    ToggleRead,
+    ToggleStar,
+    Archive,
+    Delete,
+    Spam,
+    Move
+}
diff --git a/app/core/src/main/java/com/fsck/k9/preferences/GeneralSettingsDescriptions.java b/app/core/src/main/java/com/fsck/k9/preferences/GeneralSettingsDescriptions.java
index ce5fa8e1e9f..93dd1d23ca0 100644
--- a/app/core/src/main/java/com/fsck/k9/preferences/GeneralSettingsDescriptions.java
+++ b/app/core/src/main/java/com/fsck/k9/preferences/GeneralSettingsDescriptions.java
@@ -10,7 +10,6 @@
 import java.util.TreeMap;
 
 import android.content.Context;
-import android.graphics.Color;
 
 import com.fsck.k9.Account;
 import com.fsck.k9.Account.SortType;
@@ -20,6 +19,7 @@
 import com.fsck.k9.K9.BACKGROUND_OPS;
 import com.fsck.k9.K9.NotificationQuickDelete;
 import com.fsck.k9.K9.SplitViewMode;
+import com.fsck.k9.SwipeAction;
 import com.fsck.k9.core.R;
 import com.fsck.k9.preferences.Settings.BooleanSetting;
 import com.fsck.k9.preferences.Settings.ColorSetting;
@@ -277,6 +277,12 @@ public class GeneralSettingsDescriptions {
         s.put("showStarredCount", Settings.versions(
                 new V(75, new BooleanSetting(false))
         ));
+        s.put("swipeRightAction", Settings.versions(
+                new V(83, new EnumSetting<>(SwipeAction.class, SwipeAction.ToggleSelection))
+        ));
+        s.put("swipeLeftAction", Settings.versions(
+                new V(83, new EnumSetting<>(SwipeAction.class, SwipeAction.ToggleRead))
+        ));
 
         SETTINGS = Collections.unmodifiableMap(s);
 
diff --git a/app/core/src/main/res/values/arrays_general_settings_values.xml b/app/core/src/main/res/values/arrays_general_settings_values.xml
index b5b0f49f67a..95fca883f9a 100644
--- a/app/core/src/main/res/values/arrays_general_settings_values.xml
+++ b/app/core/src/main/res/values/arrays_general_settings_values.xml
@@ -210,4 +210,15 @@
         <item>spam</item>
     </string-array>
 
+    <string-array name="swipe_action_values" translatable="false">
+        <item>none</item>
+        <item>toggle_selection</item>
+        <item>toggle_read</item>
+        <item>toggle_star</item>
+        <item>archive</item>
+        <item>delete</item>
+        <item>spam</item>
+        <item>move</item>
+    </string-array>
+
 </resources>
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 0a18249e8bd..79f928ec363 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
@@ -4,7 +4,6 @@ import com.fsck.k9.account.accountModule
 import com.fsck.k9.activity.activityModule
 import com.fsck.k9.autodiscovery.providersxml.autodiscoveryProvidersXmlModule
 import com.fsck.k9.contacts.contactsModule
-import com.fsck.k9.fragment.fragmentModule
 import com.fsck.k9.ui.account.accountUiModule
 import com.fsck.k9.ui.base.uiBaseModule
 import com.fsck.k9.ui.changelog.changelogUiModule
@@ -28,7 +27,6 @@ val uiModules = listOf(
     messageListUiModule,
     manageFoldersUiModule,
     chooseFolderUiModule,
-    fragmentModule,
     contactsModule,
     accountModule,
     autodiscoveryProvidersXmlModule,
diff --git a/app/ui/legacy/src/main/java/com/fsck/k9/activity/MessageList.kt b/app/ui/legacy/src/main/java/com/fsck/k9/activity/MessageList.kt
index e5d9dbcf726..dfdcd8fad6a 100644
--- a/app/ui/legacy/src/main/java/com/fsck/k9/activity/MessageList.kt
+++ b/app/ui/legacy/src/main/java/com/fsck/k9/activity/MessageList.kt
@@ -34,8 +34,6 @@ import com.fsck.k9.account.BackgroundAccountRemover
 import com.fsck.k9.activity.compose.MessageActions
 import com.fsck.k9.controller.MessageReference
 import com.fsck.k9.controller.MessagingController
-import com.fsck.k9.fragment.MessageListFragment
-import com.fsck.k9.fragment.MessageListFragment.MessageListFragmentListener
 import com.fsck.k9.helper.Contacts
 import com.fsck.k9.helper.ParcelableUtil
 import com.fsck.k9.mailstore.SearchStatusManager
@@ -54,6 +52,8 @@ import com.fsck.k9.ui.changelog.RecentChangesActivity
 import com.fsck.k9.ui.changelog.RecentChangesViewModel
 import com.fsck.k9.ui.managefolders.ManageFoldersActivity
 import com.fsck.k9.ui.messagelist.DefaultFolderProvider
+import com.fsck.k9.ui.messagelist.MessageListFragment
+import com.fsck.k9.ui.messagelist.MessageListFragment.MessageListFragmentListener
 import com.fsck.k9.ui.messageview.Direction
 import com.fsck.k9.ui.messageview.MessageViewContainerFragment
 import com.fsck.k9.ui.messageview.MessageViewContainerFragment.MessageViewContainerListener
@@ -119,7 +119,7 @@ open class MessageList :
                 ?: if (K9.isMessageViewShowNext) Direction.NEXT else Direction.PREVIOUS
         }
 
-    private var messageListActivityAppearance: MessageListActivityAppearance? = null
+    private var messageListActivityConfig: MessageListActivityConfig? = null
 
     /**
      * `true` if the message list should be displayed as flat list (i.e. no threading)
@@ -548,9 +548,9 @@ open class MessageList :
     public override fun onResume() {
         super.onResume()
 
-        if (messageListActivityAppearance == null) {
-            messageListActivityAppearance = MessageListActivityAppearance.create(generalSettingsManager)
-        } else if (messageListActivityAppearance != MessageListActivityAppearance.create(generalSettingsManager)) {
+        if (messageListActivityConfig == null) {
+            messageListActivityConfig = MessageListActivityConfig.create(generalSettingsManager)
+        } else if (messageListActivityConfig != MessageListActivityConfig.create(generalSettingsManager)) {
             recreateCompat()
         }
 
diff --git a/app/ui/legacy/src/main/java/com/fsck/k9/activity/MessageListActivityAppearance.kt b/app/ui/legacy/src/main/java/com/fsck/k9/activity/MessageListActivityConfig.kt
similarity index 88%
rename from app/ui/legacy/src/main/java/com/fsck/k9/activity/MessageListActivityAppearance.kt
rename to app/ui/legacy/src/main/java/com/fsck/k9/activity/MessageListActivityConfig.kt
index fac66de81fa..fc2ebd1ec5f 100644
--- a/app/ui/legacy/src/main/java/com/fsck/k9/activity/MessageListActivityAppearance.kt
+++ b/app/ui/legacy/src/main/java/com/fsck/k9/activity/MessageListActivityConfig.kt
@@ -1,11 +1,12 @@
 package com.fsck.k9.activity
 
 import com.fsck.k9.K9
+import com.fsck.k9.SwipeAction
 import com.fsck.k9.preferences.AppTheme
 import com.fsck.k9.preferences.GeneralSettingsManager
 import com.fsck.k9.preferences.SubTheme
 
-data class MessageListActivityAppearance(
+data class MessageListActivityConfig(
     val appTheme: AppTheme,
     val isShowUnifiedInbox: Boolean,
     val isShowMessageListStars: Boolean,
@@ -31,13 +32,15 @@ data class MessageListActivityAppearance(
     val fontSizeMessageViewAdditionalHeaders: Int,
     val fontSizeMessageViewSubject: Int,
     val fontSizeMessageViewDate: Int,
-    val fontSizeMessageViewContentAsPercent: Int
+    val fontSizeMessageViewContentAsPercent: Int,
+    val swipeRightAction: SwipeAction,
+    val swipeLeftAction: SwipeAction,
 ) {
 
     companion object {
-        fun create(generalSettingsManager: GeneralSettingsManager): MessageListActivityAppearance {
+        fun create(generalSettingsManager: GeneralSettingsManager): MessageListActivityConfig {
             val settings = generalSettingsManager.getSettings()
-            return MessageListActivityAppearance(
+            return MessageListActivityConfig(
                 appTheme = settings.appTheme,
                 isShowUnifiedInbox = K9.isShowUnifiedInbox,
                 isShowMessageListStars = K9.isShowMessageListStars,
@@ -63,7 +66,9 @@ data class MessageListActivityAppearance(
                 fontSizeMessageViewAdditionalHeaders = K9.fontSizes.messageViewAdditionalHeaders,
                 fontSizeMessageViewSubject = K9.fontSizes.messageViewSubject,
                 fontSizeMessageViewDate = K9.fontSizes.messageViewDate,
-                fontSizeMessageViewContentAsPercent = K9.fontSizes.messageViewContentAsPercent
+                fontSizeMessageViewContentAsPercent = K9.fontSizes.messageViewContentAsPercent,
+                swipeRightAction = K9.swipeRightAction,
+                swipeLeftAction = K9.swipeLeftAction,
             )
         }
     }
diff --git a/app/ui/legacy/src/main/java/com/fsck/k9/fragment/KoinModule.kt b/app/ui/legacy/src/main/java/com/fsck/k9/fragment/KoinModule.kt
deleted file mode 100644
index 7fe88ce2f52..00000000000
--- a/app/ui/legacy/src/main/java/com/fsck/k9/fragment/KoinModule.kt
+++ /dev/null
@@ -1,7 +0,0 @@
-package com.fsck.k9.fragment
-
-import org.koin.dsl.module
-
-val fragmentModule = module {
-    single { SortTypeToastProvider() }
-}
diff --git a/app/ui/legacy/src/main/java/com/fsck/k9/ui/messagelist/KoinModule.kt b/app/ui/legacy/src/main/java/com/fsck/k9/ui/messagelist/KoinModule.kt
index 16be0694ae9..487fcea1c55 100644
--- a/app/ui/legacy/src/main/java/com/fsck/k9/ui/messagelist/KoinModule.kt
+++ b/app/ui/legacy/src/main/java/com/fsck/k9/ui/messagelist/KoinModule.kt
@@ -17,4 +17,5 @@ val messageListUiModule = module {
     factory {
         MessageListLiveDataFactory(messageListLoader = get(), preferences = get(), messageListRepository = get())
     }
+    single { SortTypeToastProvider() }
 }
diff --git a/app/ui/legacy/src/main/java/com/fsck/k9/fragment/MessageListAdapter.kt b/app/ui/legacy/src/main/java/com/fsck/k9/ui/messagelist/MessageListAdapter.kt
similarity index 98%
rename from app/ui/legacy/src/main/java/com/fsck/k9/fragment/MessageListAdapter.kt
rename to app/ui/legacy/src/main/java/com/fsck/k9/ui/messagelist/MessageListAdapter.kt
index f9ee7cf1e44..afd7661ef70 100644
--- a/app/ui/legacy/src/main/java/com/fsck/k9/fragment/MessageListAdapter.kt
+++ b/app/ui/legacy/src/main/java/com/fsck/k9/ui/messagelist/MessageListAdapter.kt
@@ -1,4 +1,4 @@
-package com.fsck.k9.fragment
+package com.fsck.k9.ui.messagelist
 
 import android.annotation.SuppressLint
 import android.content.res.Resources
@@ -28,8 +28,6 @@ import com.fsck.k9.controller.MessageReference
 import com.fsck.k9.mail.Address
 import com.fsck.k9.ui.R
 import com.fsck.k9.ui.helper.RelativeDateTimeFormatter
-import com.fsck.k9.ui.messagelist.MessageListAppearance
-import com.fsck.k9.ui.messagelist.MessageListItem
 import com.fsck.k9.ui.resolveColorAttribute
 import com.fsck.k9.ui.resolveDrawableAttribute
 import kotlin.math.max
@@ -221,8 +219,8 @@ class MessageListAdapter internal constructor(
 
     private fun getItem(position: Int): MessageListItem = messages[position]
 
-    fun getItemById(uniqueId: Long): MessageListItem {
-        return messagesMap[uniqueId]!!
+    fun getItemById(uniqueId: Long): MessageListItem? {
+        return messagesMap[uniqueId]
     }
 
     fun getItem(messageReference: MessageReference): MessageListItem? {
@@ -526,7 +524,7 @@ class MessageListAdapter internal constructor(
 
     private fun getItemFromView(view: View): MessageListItem {
         val messageViewHolder = view.tag as MessageViewHolder
-        return getItemById(messageViewHolder.uniqueId)
+        return getItemById(messageViewHolder.uniqueId) ?: error("Couldn't find MessageListItem by View")
     }
 }
 
diff --git a/app/ui/legacy/src/main/java/com/fsck/k9/fragment/MessageListFragment.kt b/app/ui/legacy/src/main/java/com/fsck/k9/ui/messagelist/MessageListFragment.kt
similarity index 95%
rename from app/ui/legacy/src/main/java/com/fsck/k9/fragment/MessageListFragment.kt
rename to app/ui/legacy/src/main/java/com/fsck/k9/ui/messagelist/MessageListFragment.kt
index e4e68904ef5..5173e52e4a7 100644
--- a/app/ui/legacy/src/main/java/com/fsck/k9/fragment/MessageListFragment.kt
+++ b/app/ui/legacy/src/main/java/com/fsck/k9/ui/messagelist/MessageListFragment.kt
@@ -1,4 +1,4 @@
-package com.fsck.k9.fragment
+package com.fsck.k9.ui.messagelist
 
 import android.app.Activity
 import android.app.SearchManager
@@ -16,7 +16,7 @@ import android.widget.Toast
 import androidx.appcompat.view.ActionMode
 import androidx.core.os.bundleOf
 import androidx.fragment.app.Fragment
-import androidx.recyclerview.widget.DefaultItemAnimator
+import androidx.recyclerview.widget.ItemTouchHelper
 import androidx.recyclerview.widget.LinearLayoutManager
 import androidx.recyclerview.widget.RecyclerView
 import androidx.swiperefreshlayout.widget.SwipeRefreshLayout
@@ -26,14 +26,15 @@ import com.fsck.k9.Account.SortType
 import com.fsck.k9.Clock
 import com.fsck.k9.K9
 import com.fsck.k9.Preferences
+import com.fsck.k9.SwipeAction
 import com.fsck.k9.activity.FolderInfoHolder
 import com.fsck.k9.activity.Search
 import com.fsck.k9.activity.misc.ContactPicture
 import com.fsck.k9.controller.MessageReference
 import com.fsck.k9.controller.MessagingController
 import com.fsck.k9.controller.SimpleMessagingListener
+import com.fsck.k9.fragment.ConfirmationDialogFragment
 import com.fsck.k9.fragment.ConfirmationDialogFragment.ConfirmationDialogFragmentListener
-import com.fsck.k9.fragment.MessageListFragment.MessageListFragmentListener.Companion.MAX_PROGRESS
 import com.fsck.k9.helper.Utility
 import com.fsck.k9.helper.mapToSet
 import com.fsck.k9.mail.Flag
@@ -46,13 +47,7 @@ import com.fsck.k9.ui.choosefolder.ChooseFolderActivity
 import com.fsck.k9.ui.folders.FolderNameFormatter
 import com.fsck.k9.ui.folders.FolderNameFormatterFactory
 import com.fsck.k9.ui.helper.RelativeDateTimeFormatter
-import com.fsck.k9.ui.messagelist.MessageListAppearance
-import com.fsck.k9.ui.messagelist.MessageListConfig
-import com.fsck.k9.ui.messagelist.MessageListInfo
-import com.fsck.k9.ui.messagelist.MessageListItem
-import com.fsck.k9.ui.messagelist.MessageListItemDecoration
-import com.fsck.k9.ui.messagelist.MessageListViewModel
-import com.fsck.k9.ui.messagelist.MessageSortOverride
+import com.fsck.k9.ui.messagelist.MessageListFragment.MessageListFragmentListener.Companion.MAX_PROGRESS
 import java.util.concurrent.Future
 import net.jcip.annotations.GuardedBy
 import org.koin.android.ext.android.inject
@@ -242,9 +237,8 @@ class MessageListFragment :
 
         val itemDecoration = MessageListItemDecoration(requireContext())
         recyclerView.addItemDecoration(itemDecoration)
-        recyclerView.itemAnimator = DefaultItemAnimator().apply {
-            supportsChangeAnimations = false
-        }
+
+        recyclerView.itemAnimator = MessageListItemAnimator()
     }
 
     override fun onActivityCreated(savedInstanceState: Bundle?) {
@@ -258,8 +252,10 @@ class MessageListFragment :
     }
 
     private fun initializeMessageList() {
+        val theme = requireActivity().theme
+
         adapter = MessageListAdapter(
-            theme = requireActivity().theme,
+            theme = theme,
             res = resources,
             layoutInflater = layoutInflater,
             contactsPictureLoader = ContactPicture.getContactPictureLoader(),
@@ -271,6 +267,19 @@ class MessageListFragment :
         adapter.activeMessage = activeMessage
 
         recyclerView.adapter = adapter
+
+        val itemTouchHelper = ItemTouchHelper(
+            MessageListSwipeCallback(
+                resources,
+                resourceProvider = SwipeResourceProvider(theme),
+                swipeActionSupportProvider,
+                swipeRightAction = K9.swipeRightAction,
+                swipeLeftAction = K9.swipeLeftAction,
+                adapter,
+                swipeListener
+            )
+        )
+        itemTouchHelper.attachToRecyclerView(recyclerView)
     }
 
     private fun initializeSortSettings() {
@@ -1420,6 +1429,58 @@ class MessageListFragment :
     private val isPullToRefreshAllowed: Boolean
         get() = isRemoteSearchAllowed || isCheckMailAllowed
 
+    private val swipeListener = MessageListSwipeListener { item, action ->
+        when (action) {
+            SwipeAction.None -> Unit
+            SwipeAction.ToggleSelection -> {
+                toggleMessageSelect(item)
+            }
+            SwipeAction.ToggleRead -> {
+                setFlag(item, Flag.SEEN, !item.isRead)
+            }
+            SwipeAction.ToggleStar -> {
+                setFlag(item, Flag.FLAGGED, !item.isStarred)
+            }
+            SwipeAction.Archive -> {
+                onArchive(item.messageReference)
+            }
+            SwipeAction.Delete -> {
+                if (K9.isConfirmDelete) {
+                    notifyItemChanged(item)
+                }
+                onDelete(listOf(item.messageReference))
+            }
+            SwipeAction.Spam -> {
+                if (K9.isConfirmSpam) {
+                    notifyItemChanged(item)
+                }
+                onSpam(listOf(item.messageReference))
+            }
+            SwipeAction.Move -> {
+                notifyItemChanged(item)
+                onMove(item.messageReference)
+            }
+        }
+    }
+
+    private fun notifyItemChanged(item: MessageListItem) {
+        val position = adapter.getPosition(item) ?: return
+        adapter.notifyItemChanged(position)
+    }
+
+    private val swipeActionSupportProvider = SwipeActionSupportProvider { item, action ->
+        when (action) {
+            SwipeAction.None -> false
+            SwipeAction.ToggleSelection -> true
+            SwipeAction.ToggleRead -> !isOutbox
+            SwipeAction.ToggleStar -> !isOutbox
+            SwipeAction.Archive -> !isOutbox && item.account.hasArchiveFolder()
+            SwipeAction.Delete -> true
+            SwipeAction.Move -> !isOutbox && messagingController.isMoveCapable(item.account)
+            SwipeAction.Spam -> !isOutbox && item.account.hasSpamFolder() && item.folderId != item.account.spamFolderId
+        }
+    }
+
     internal inner class MessageListActivityListener : SimpleMessagingListener() {
         private val lock = Any()
 
diff --git a/app/ui/legacy/src/main/java/com/fsck/k9/fragment/MessageListHandler.java b/app/ui/legacy/src/main/java/com/fsck/k9/ui/messagelist/MessageListHandler.java
similarity index 99%
rename from app/ui/legacy/src/main/java/com/fsck/k9/fragment/MessageListHandler.java
rename to app/ui/legacy/src/main/java/com/fsck/k9/ui/messagelist/MessageListHandler.java
index 72252cf15fb..22a7a29826f 100644
--- a/app/ui/legacy/src/main/java/com/fsck/k9/fragment/MessageListHandler.java
+++ b/app/ui/legacy/src/main/java/com/fsck/k9/ui/messagelist/MessageListHandler.java
@@ -1,4 +1,4 @@
-package com.fsck.k9.fragment;
+package com.fsck.k9.ui.messagelist;
 
 
 import java.lang.ref.WeakReference;
diff --git a/app/ui/legacy/src/main/java/com/fsck/k9/ui/messagelist/MessageListItemAnimator.kt b/app/ui/legacy/src/main/java/com/fsck/k9/ui/messagelist/MessageListItemAnimator.kt
new file mode 100644
index 00000000000..ad9ec900cdf
--- /dev/null
+++ b/app/ui/legacy/src/main/java/com/fsck/k9/ui/messagelist/MessageListItemAnimator.kt
@@ -0,0 +1,17 @@
+package com.fsck.k9.ui.messagelist
+
+import androidx.recyclerview.widget.DefaultItemAnimator
+import androidx.recyclerview.widget.RecyclerView.ViewHolder
+
+class MessageListItemAnimator : DefaultItemAnimator() {
+    init {
+        supportsChangeAnimations = false
+        changeDuration = 120
+    }
+
+    override fun canReuseUpdatedViewHolder(viewHolder: ViewHolder, payloads: MutableList<Any>): Boolean {
+        // ItemTouchHelper expects swiped views to be removed from the view hierarchy. So we don't reuse views that are
+        // marked as having been swiped.
+        return !viewHolder.wasSwiped && super.canReuseUpdatedViewHolder(viewHolder, payloads)
+    }
+}
diff --git a/app/ui/legacy/src/main/java/com/fsck/k9/ui/messagelist/MessageListSwipeCallback.kt b/app/ui/legacy/src/main/java/com/fsck/k9/ui/messagelist/MessageListSwipeCallback.kt
new file mode 100644
index 00000000000..20e665fe692
--- /dev/null
+++ b/app/ui/legacy/src/main/java/com/fsck/k9/ui/messagelist/MessageListSwipeCallback.kt
@@ -0,0 +1,172 @@
+package com.fsck.k9.ui.messagelist
+
+import android.content.res.Resources
+import android.graphics.Canvas
+import android.graphics.Paint
+import android.view.View
+import androidx.core.graphics.withSave
+import androidx.recyclerview.widget.ItemTouchHelper
+import androidx.recyclerview.widget.RecyclerView
+import androidx.recyclerview.widget.RecyclerView.ViewHolder
+import com.fsck.k9.SwipeAction
+import com.fsck.k9.ui.R
+import kotlin.math.abs
+
+class MessageListSwipeCallback(
+    resources: Resources,
+    private val resourceProvider: SwipeResourceProvider,
+    private val swipeActionSupportProvider: SwipeActionSupportProvider,
+    private val swipeRightAction: SwipeAction,
+    private val swipeLeftAction: SwipeAction,
+    private val adapter: MessageListAdapter,
+    private val listener: MessageListSwipeListener
+) : ItemTouchHelper.Callback() {
+    private val iconPadding = resources.getDimension(R.dimen.messageListSwipeIconPadding).toInt()
+    private val swipeThreshold = resources.getDimension(R.dimen.messageListSwipeThreshold)
+    private val backgroundColorPaint = Paint()
+
+    override fun getMovementFlags(recyclerView: RecyclerView, viewHolder: ViewHolder): Int {
+        if (viewHolder !is MessageViewHolder) return 0
+
+        val item = adapter.getItemById(viewHolder.uniqueId) ?: return 0
+
+        var swipeFlags = 0
+        if (swipeActionSupportProvider.isActionSupported(item, swipeRightAction)) {
+            swipeFlags = swipeFlags or ItemTouchHelper.RIGHT
+        }
+        if (swipeActionSupportProvider.isActionSupported(item, swipeLeftAction)) {
+            swipeFlags = swipeFlags or ItemTouchHelper.LEFT
+        }
+
+        return makeMovementFlags(0, swipeFlags)
+    }
+
+    override fun onMove(
+        recyclerView: RecyclerView,
+        viewHolder: ViewHolder,
+        target: ViewHolder
+    ): Boolean {
+        throw UnsupportedOperationException("not implemented")
+    }
+
+    override fun onSwiped(viewHolder: ViewHolder, direction: Int) {
+        val holder = viewHolder as MessageViewHolder
+        val item = adapter.getItemById(holder.uniqueId) ?: error("Couldn't find MessageListItem")
+
+        // ItemTouchHelper expects swiped views to be removed from the view hierarchy. We mark this ViewHolder so that
+        // MessageListItemAnimator knows not to reuse it during an animation.
+        viewHolder.markAsSwiped(true)
+
+        when (direction) {
+            ItemTouchHelper.RIGHT -> listener.onSwipeAction(item, swipeRightAction)
+            ItemTouchHelper.LEFT -> listener.onSwipeAction(item, swipeLeftAction)
+            else -> error("Unsupported direction: $direction")
+        }
+    }
+
+    override fun clearView(recyclerView: RecyclerView, viewHolder: ViewHolder) {
+        super.clearView(recyclerView, viewHolder)
+        viewHolder.markAsSwiped(false)
+    }
+
+    override fun getSwipeThreshold(viewHolder: ViewHolder): Float {
+        return swipeThreshold / viewHolder.itemView.width
+    }
+
+    override fun onChildDraw(
+        canvas: Canvas,
+        recyclerView: RecyclerView,
+        viewHolder: ViewHolder,
+        dX: Float,
+        dY: Float,
+        actionState: Int,
+        isCurrentlyActive: Boolean
+    ) {
+        canvas.withSave {
+            val view = viewHolder.itemView
+
+            val holder = viewHolder as MessageViewHolder
+            val item = adapter.getItemById(holder.uniqueId) ?: return@withSave
+
+            val swipeThreshold = recyclerView.width * getSwipeThreshold(holder)
+            val swipeThresholdReached = abs(dX) > swipeThreshold
+            if (swipeThresholdReached) {
+                val action = if (dX > 0) swipeRightAction else swipeLeftAction
+                val backgroundColor = resourceProvider.getBackgroundColor(item, action)
+                drawBackground(view, backgroundColor)
+            } else {
+                val backgroundColor = resourceProvider.getBackgroundColor(item, SwipeAction.None)
+                drawBackground(view, backgroundColor)
+            }
+
+            // Stop drawing the icon when the view has been animated all the way off the screen by ItemTouchHelper.
+            // We do this so the icon doesn't switch state when RecyclerView's ItemAnimator animates the view back after
+            // a toggle action (mark as read/unread, add/remove star) was used.
+            if (isCurrentlyActive || abs(dX).toInt() < view.width) {
+                drawIcon(dX, view, item, swipeThresholdReached)
+            }
+        }
+
+        super.onChildDraw(canvas, recyclerView, viewHolder, dX, dY, actionState, isCurrentlyActive)
+    }
+
+    private fun Canvas.drawBackground(view: View, color: Int) {
+        backgroundColorPaint.color = color
+        drawRect(
+            view.left.toFloat(),
+            view.top.toFloat(),
+            view.right.toFloat(),
+            view.bottom.toFloat(),
+            backgroundColorPaint
+        )
+    }
+
+    private fun Canvas.drawIcon(dX: Float, view: View, item: MessageListItem, swipeThresholdReached: Boolean) {
+        if (dX > 0) {
+            drawSwipeRightIcon(view, item, swipeThresholdReached)
+        } else {
+            drawSwipeLeftIcon(view, item, swipeThresholdReached)
+        }
+    }
+
+    private fun Canvas.drawSwipeRightIcon(view: View, item: MessageListItem, swipeThresholdReached: Boolean) {
+        resourceProvider.getIcon(item, swipeRightAction)?.let { icon ->
+            val iconLeft = iconPadding
+            val iconTop = view.top + ((view.height - icon.intrinsicHeight) / 2)
+            val iconRight = iconLeft + icon.intrinsicWidth
+            val iconBottom = iconTop + icon.intrinsicHeight
+            icon.setBounds(iconLeft, iconTop, iconRight, iconBottom)
+
+            icon.setTint(resourceProvider.getIconTint(item, swipeRightAction, swipeThresholdReached))
+            icon.draw(this)
+        }
+    }
+
+    private fun Canvas.drawSwipeLeftIcon(view: View, item: MessageListItem, swipeThresholdReached: Boolean) {
+        resourceProvider.getIcon(item, swipeLeftAction)?.let { icon ->
+            val iconRight = view.right - iconPadding
+            val iconLeft = iconRight - icon.intrinsicWidth
+            val iconTop = view.top + ((view.height - icon.intrinsicHeight) / 2)
+            val iconBottom = iconTop + icon.intrinsicHeight
+            icon.setBounds(iconLeft, iconTop, iconRight, iconBottom)
+
+            icon.setTint(resourceProvider.getIconTint(item, swipeLeftAction, swipeThresholdReached))
+            icon.draw(this)
+        }
+    }
+}
+
+fun interface SwipeActionSupportProvider {
+    fun isActionSupported(item: MessageListItem, action: SwipeAction): Boolean
+}
+
+fun interface MessageListSwipeListener {
+    fun onSwipeAction(item: MessageListItem, action: SwipeAction)
+}
+
+private fun ViewHolder.markAsSwiped(value: Boolean) {
+    itemView.setTag(R.id.message_list_swipe_tag, if (value) true else null)
+}
+
+val ViewHolder.wasSwiped
+    get() = itemView.getTag(R.id.message_list_swipe_tag) == true
diff --git a/app/ui/legacy/src/main/java/com/fsck/k9/fragment/MessageViewHolder.kt b/app/ui/legacy/src/main/java/com/fsck/k9/ui/messagelist/MessageListViewHolder.kt
similarity index 96%
rename from app/ui/legacy/src/main/java/com/fsck/k9/fragment/MessageViewHolder.kt
rename to app/ui/legacy/src/main/java/com/fsck/k9/ui/messagelist/MessageListViewHolder.kt
index 5e436a3c73e..803d08c3d1f 100644
--- a/app/ui/legacy/src/main/java/com/fsck/k9/fragment/MessageViewHolder.kt
+++ b/app/ui/legacy/src/main/java/com/fsck/k9/ui/messagelist/MessageListViewHolder.kt
@@ -1,4 +1,4 @@
-package com.fsck.k9.fragment
+package com.fsck.k9.ui.messagelist
 
 import android.view.View
 import android.widget.CheckBox
diff --git a/app/ui/legacy/src/main/java/com/fsck/k9/fragment/MlfUtils.java b/app/ui/legacy/src/main/java/com/fsck/k9/ui/messagelist/MlfUtils.java
similarity index 97%
rename from app/ui/legacy/src/main/java/com/fsck/k9/fragment/MlfUtils.java
rename to app/ui/legacy/src/main/java/com/fsck/k9/ui/messagelist/MlfUtils.java
index eb35a43e3bf..9c9522b42cd 100644
--- a/app/ui/legacy/src/main/java/com/fsck/k9/fragment/MlfUtils.java
+++ b/app/ui/legacy/src/main/java/com/fsck/k9/ui/messagelist/MlfUtils.java
@@ -1,4 +1,4 @@
-package com.fsck.k9.fragment;
+package com.fsck.k9.ui.messagelist;
 
 
 import java.util.List;
diff --git a/app/ui/legacy/src/main/java/com/fsck/k9/fragment/SortTypeToastProvider.kt b/app/ui/legacy/src/main/java/com/fsck/k9/ui/messagelist/SortTypeToastProvider.kt
similarity index 97%
rename from app/ui/legacy/src/main/java/com/fsck/k9/fragment/SortTypeToastProvider.kt
rename to app/ui/legacy/src/main/java/com/fsck/k9/ui/messagelist/SortTypeToastProvider.kt
index 998c9ed6235..b4774b2df18 100644
--- a/app/ui/legacy/src/main/java/com/fsck/k9/fragment/SortTypeToastProvider.kt
+++ b/app/ui/legacy/src/main/java/com/fsck/k9/ui/messagelist/SortTypeToastProvider.kt
@@ -1,4 +1,4 @@
-package com.fsck.k9.fragment
+package com.fsck.k9.ui.messagelist
 
 import com.fsck.k9.Account.SortType
 import com.fsck.k9.Account.SortType.SORT_ARRIVAL
diff --git a/app/ui/legacy/src/main/java/com/fsck/k9/ui/messagelist/SwipeResourceProvider.kt b/app/ui/legacy/src/main/java/com/fsck/k9/ui/messagelist/SwipeResourceProvider.kt
new file mode 100644
index 00000000000..d61064c9d2d
--- /dev/null
+++ b/app/ui/legacy/src/main/java/com/fsck/k9/ui/messagelist/SwipeResourceProvider.kt
@@ -0,0 +1,72 @@
+package com.fsck.k9.ui.messagelist
+
+import android.content.res.Resources.Theme
+import android.graphics.drawable.Drawable
+import androidx.annotation.AttrRes
+import com.fsck.k9.SwipeAction
+import com.fsck.k9.ui.R
+import com.fsck.k9.ui.resolveColorAttribute
+import com.fsck.k9.ui.resolveDrawableAttribute
+
+class SwipeResourceProvider(val theme: Theme) {
+    private val iconTint = theme.resolveColorAttribute(R.attr.messageListSwipeIconTint)
+
+    private val selectIcon = theme.loadDrawable(R.attr.messageListSwipeSelectIcon)
+    private val markAsReadIcon = theme.loadDrawable(R.attr.messageListSwipeMarkAsReadIcon)
+    private val markAsUnreadIcon = theme.loadDrawable(R.attr.messageListSwipeMarkAsUnreadIcon)
+    private val addStarIcon = theme.loadDrawable(R.attr.messageListSwipeAddStarIcon)
+    private val removeStarIcon = theme.loadDrawable(R.attr.messageListSwipeRemoveStarIcon)
+    private val archiveIcon = theme.loadDrawable(R.attr.messageListSwipeArchiveIcon)
+    private val deleteIcon = theme.loadDrawable(R.attr.messageListSwipeDeleteIcon)
+    private val spamIcon = theme.loadDrawable(R.attr.messageListSwipeSpamIcon)
+    private val moveIcon = theme.loadDrawable(R.attr.messageListSwipeMoveIcon)
+
+    private val noActionColor = theme.resolveColorAttribute(R.attr.messageListSwipeDisabledBackgroundColor)
+    private val selectColor = theme.resolveColorAttribute(R.attr.messageListSwipeSelectBackgroundColor)
+    private val markAsReadColor = theme.resolveColorAttribute(R.attr.messageListSwipeMarkAsReadBackgroundColor)
+    private val markAsUnreadColor = theme.resolveColorAttribute(R.attr.messageListSwipeMarkAsUnreadBackgroundColor)
+    private val addStarColor = theme.resolveColorAttribute(R.attr.messageListSwipeAddStarBackgroundColor)
+    private val removeStarColor = theme.resolveColorAttribute(R.attr.messageListSwipeRemoveStarBackgroundColor)
+    private val archiveColor = theme.resolveColorAttribute(R.attr.messageListSwipeArchiveBackgroundColor)
+    private val deleteColor = theme.resolveColorAttribute(R.attr.messageListSwipeDeleteBackgroundColor)
+    private val spamColor = theme.resolveColorAttribute(R.attr.messageListSwipeSpamBackgroundColor)
+    private val moveColor = theme.resolveColorAttribute(R.attr.messageListSwipeMoveBackgroundColor)
+
+    fun getIconTint(item: MessageListItem, action: SwipeAction, swipeThresholdReached: Boolean): Int {
+        return if (swipeThresholdReached) {
+            iconTint
+        } else {
+            getBackgroundColor(item, action)
+        }
+    }
+
+    fun getIcon(item: MessageListItem, action: SwipeAction): Drawable? {
+        return when (action) {
+            SwipeAction.None -> null
+            SwipeAction.ToggleSelection -> selectIcon
+            SwipeAction.ToggleRead -> if (item.isRead) markAsUnreadIcon else markAsReadIcon
+            SwipeAction.ToggleStar -> if (item.isStarred) removeStarIcon else addStarIcon
+            SwipeAction.Archive -> archiveIcon
+            SwipeAction.Delete -> deleteIcon
+            SwipeAction.Spam -> spamIcon
+            SwipeAction.Move -> moveIcon
+        }
+    }
+
+    fun getBackgroundColor(item: MessageListItem, action: SwipeAction): Int {
+        return when (action) {
+            SwipeAction.None -> noActionColor
+            SwipeAction.ToggleSelection -> selectColor
+            SwipeAction.ToggleRead -> if (item.isRead) markAsUnreadColor else markAsReadColor
+            SwipeAction.ToggleStar -> if (item.isStarred) removeStarColor else addStarColor
+            SwipeAction.Archive -> archiveColor
+            SwipeAction.Delete -> deleteColor
+            SwipeAction.Spam -> spamColor
+            SwipeAction.Move -> moveColor
+        }
+    }
+}
+
+private fun Theme.loadDrawable(@AttrRes attributeId: Int): Drawable {
+    return resolveDrawableAttribute(attributeId).mutate()
+}
diff --git a/app/ui/legacy/src/main/java/com/fsck/k9/ui/settings/general/GeneralSettingsDataStore.kt b/app/ui/legacy/src/main/java/com/fsck/k9/ui/settings/general/GeneralSettingsDataStore.kt
index d712a8cde7c..6e1ae5a9c49 100644
--- a/app/ui/legacy/src/main/java/com/fsck/k9/ui/settings/general/GeneralSettingsDataStore.kt
+++ b/app/ui/legacy/src/main/java/com/fsck/k9/ui/settings/general/GeneralSettingsDataStore.kt
@@ -2,6 +2,7 @@ package com.fsck.k9.ui.settings.general
 
 import androidx.preference.PreferenceDataStore
 import com.fsck.k9.K9
+import com.fsck.k9.SwipeAction
 import com.fsck.k9.job.K9JobManager
 import com.fsck.k9.preferences.AppTheme
 import com.fsck.k9.preferences.GeneralSettingsManager
@@ -125,6 +126,8 @@ class GeneralSettingsDataStore(
             "message_view_date_font" -> K9.fontSizes.messageViewDate.toString()
             "message_view_additional_headers_font" -> K9.fontSizes.messageViewAdditionalHeaders.toString()
             "message_compose_input_font" -> K9.fontSizes.messageComposeInput.toString()
+            "swipe_action_right" -> swipeActionToString(K9.swipeRightAction)
+            "swipe_action_left" -> swipeActionToString(K9.swipeLeftAction)
             else -> defValue
         }
     }
@@ -164,6 +167,8 @@ class GeneralSettingsDataStore(
             "message_view_date_font" -> K9.fontSizes.messageViewDate = value.toInt()
             "message_view_additional_headers_font" -> K9.fontSizes.messageViewAdditionalHeaders = value.toInt()
             "message_compose_input_font" -> K9.fontSizes.messageComposeInput = value.toInt()
+            "swipe_action_right" -> K9.swipeRightAction = stringToSwipeAction(value)
+            "swipe_action_left" -> K9.swipeLeftAction = stringToSwipeAction(value)
             else -> return
         }
 
@@ -280,4 +285,27 @@ class GeneralSettingsDataStore(
             jobManager.scheduleAllMailJobs()
         }
     }
+
+    private fun swipeActionToString(action: SwipeAction) = when (action) {
+        SwipeAction.None -> "none"
+        SwipeAction.ToggleSelection -> "toggle_selection"
+        SwipeAction.ToggleRead -> "toggle_read"
+        SwipeAction.ToggleStar -> "toggle_star"
+        SwipeAction.Archive -> "archive"
+        SwipeAction.Delete -> "delete"
+        SwipeAction.Spam -> "spam"
+        SwipeAction.Move -> "move"
+    }
+
+    private fun stringToSwipeAction(action: String) = when (action) {
+        "none" -> SwipeAction.None
+        "toggle_selection" -> SwipeAction.ToggleSelection
+        "toggle_read" -> SwipeAction.ToggleRead
+        "toggle_star" -> SwipeAction.ToggleStar
+        "archive" -> SwipeAction.Archive
+        "delete" -> SwipeAction.Delete
+        "spam" -> SwipeAction.Spam
+        "move" -> SwipeAction.Move
+        else -> throw AssertionError()
+    }
 }
diff --git a/app/ui/legacy/src/main/res/drawable/ic_move_to_folder.xml b/app/ui/legacy/src/main/res/drawable/ic_move_to_folder.xml
new file mode 100644
index 00000000000..9b88184787c
--- /dev/null
+++ b/app/ui/legacy/src/main/res/drawable/ic_move_to_folder.xml
@@ -0,0 +1,11 @@
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+    android:width="24dp"
+    android:height="24dp"
+    android:autoMirrored="true"
+    android:tint="?attr/colorControlNormal"
+    android:viewportWidth="24"
+    android:viewportHeight="24">
+    <path
+        android:fillColor="@android:color/white"
+        android:pathData="M20,6h-8l-2,-2L4,4c-1.1,0 -2,0.9 -2,2v12c0,1.1 0.9,2 2,2h16c1.1,0 2,-0.9 2,-2L22,8c0,-1.1 -0.9,-2 -2,-2zM14,18v-3h-4v-4h4L14,8l5,5 -5,5z" />
+</vector>
diff --git a/app/ui/legacy/src/main/res/layout/message_list_fragment.xml b/app/ui/legacy/src/main/res/layout/message_list_fragment.xml
index d5329bcc3ac..c0c6b931bdb 100644
--- a/app/ui/legacy/src/main/res/layout/message_list_fragment.xml
+++ b/app/ui/legacy/src/main/res/layout/message_list_fragment.xml
@@ -6,7 +6,7 @@
     android:id="@+id/swiperefresh"
     android:layout_width="match_parent"
     android:layout_height="match_parent"
-    tools:context="com.fsck.k9.fragment.MessageListFragment">
+    tools:context=".messagelist.MessageListFragment">
 
     <androidx.recyclerview.widget.RecyclerView
         android:id="@+id/message_list"
diff --git a/app/ui/legacy/src/main/res/values/arrays_general_settings_strings.xml b/app/ui/legacy/src/main/res/values/arrays_general_settings_strings.xml
index 45bf2174c22..b81a82e8a33 100644
--- a/app/ui/legacy/src/main/res/values/arrays_general_settings_strings.xml
+++ b/app/ui/legacy/src/main/res/values/arrays_general_settings_strings.xml
@@ -157,4 +157,15 @@
         <item>@string/spam_action</item>
     </string-array>
 
+    <string-array name="swipe_action_names">
+        <item>@string/general_settings_swipe_action_none</item>
+        <item>@string/general_settings_swipe_action_toggle_selection</item>
+        <item>@string/general_settings_swipe_action_toggle_read</item>
+        <item>@string/general_settings_swipe_action_toggle_star</item>
+        <item>@string/general_settings_swipe_action_archive</item>
+        <item>@string/general_settings_swipe_action_delete</item>
+        <item>@string/general_settings_swipe_action_spam</item>
+        <item>@string/general_settings_swipe_action_move</item>
+    </string-array>
+
 </resources>
diff --git a/app/ui/legacy/src/main/res/values/attrs.xml b/app/ui/legacy/src/main/res/values/attrs.xml
index fd4a8fbd380..7d93616e923 100644
--- a/app/ui/legacy/src/main/res/values/attrs.xml
+++ b/app/ui/legacy/src/main/res/values/attrs.xml
@@ -78,6 +78,26 @@
         <attr name="messageListAnswered" format="reference"/>
         <attr name="messageListForwarded" format="reference"/>
         <attr name="messageListAnsweredForwarded" format="reference"/>
+        <attr name="messageListSwipeIconTint" format="reference|color"/>
+        <attr name="messageListSwipeDisabledBackgroundColor" format="reference|color"/>
+        <attr name="messageListSwipeSelectIcon" format="reference"/>
+        <attr name="messageListSwipeSelectBackgroundColor" format="reference|color"/>
+        <attr name="messageListSwipeMarkAsReadIcon" format="reference"/>
+        <attr name="messageListSwipeMarkAsReadBackgroundColor" format="reference|color"/>
+        <attr name="messageListSwipeMarkAsUnreadIcon" format="reference"/>
+        <attr name="messageListSwipeMarkAsUnreadBackgroundColor" format="reference|color"/>
+        <attr name="messageListSwipeAddStarIcon" format="reference"/>
+        <attr name="messageListSwipeAddStarBackgroundColor" format="reference|color"/>
+        <attr name="messageListSwipeRemoveStarIcon" format="reference"/>
+        <attr name="messageListSwipeRemoveStarBackgroundColor" format="reference|color"/>
+        <attr name="messageListSwipeArchiveIcon" format="reference"/>
+        <attr name="messageListSwipeArchiveBackgroundColor" format="reference|color"/>
+        <attr name="messageListSwipeDeleteIcon" format="reference"/>
+        <attr name="messageListSwipeDeleteBackgroundColor" format="reference|color"/>
+        <attr name="messageListSwipeSpamIcon" format="reference"/>
+        <attr name="messageListSwipeSpamBackgroundColor" format="reference|color"/>
+        <attr name="messageListSwipeMoveIcon" format="reference"/>
+        <attr name="messageListSwipeMoveBackgroundColor" format="reference|color"/>
         <attr name="messageStar" format="reference"/>
         <attr name="messageStarColor" format="color"/>
         <attr name="messageViewBackgroundColor" format="reference|color"/>
diff --git a/app/ui/legacy/src/main/res/values/dimensions.xml b/app/ui/legacy/src/main/res/values/dimensions.xml
index b57ef027f00..c46128602b2 100644
--- a/app/ui/legacy/src/main/res/values/dimensions.xml
+++ b/app/ui/legacy/src/main/res/values/dimensions.xml
@@ -9,4 +9,6 @@
     <dimen name="account_setup_margin_between_items_incoming_and_outgoing">12dp</dimen>
 
     <dimen name="message_view_pager_page_margin">16dp</dimen>
+    <dimen name="messageListSwipeThreshold">72dp</dimen>
+    <dimen name="messageListSwipeIconPadding">24dp</dimen>
 </resources>
diff --git a/app/ui/legacy/src/main/res/values/ids.xml b/app/ui/legacy/src/main/res/values/ids.xml
index e308d596bde..679f0e73466 100644
--- a/app/ui/legacy/src/main/res/values/ids.xml
+++ b/app/ui/legacy/src/main/res/values/ids.xml
@@ -19,4 +19,6 @@
     <item type="id" name="settings_list_url_item"/>
 
     <item type="id" name="drawer_list_folder_item"/>
+
+    <item type="id" name="message_list_swipe_tag"/>
 </resources>
diff --git a/app/ui/legacy/src/main/res/values/strings.xml b/app/ui/legacy/src/main/res/values/strings.xml
index cf40aff93fc..3b52fa01b14 100644
--- a/app/ui/legacy/src/main/res/values/strings.xml
+++ b/app/ui/legacy/src/main/res/values/strings.xml
@@ -308,6 +308,30 @@ Please submit bug reports, contribute new features and ask questions at
     <string name="global_settings_confirm_menu_mark_all_read">Mark all messages as read</string>
     <string name="global_settings_confirm_action_delete_notif">Delete (from notification)</string>
 
+    <!-- Title of the screen to configure swipe actions -->
+    <string name="general_settings_swipe_actions_title">Swipe actions</string>
+    <!-- Name of the setting to configure the right swipe action -->
+    <string name="general_settings_right_swipe_title">Right swipe</string>
+    <!-- Name of the setting to configure the left swipe action -->
+    <string name="general_settings_left_swipe_title">Left swipe</string>
+
+    <!-- The list item users select to not use a swipe action for a particular direction -->
+    <string name="general_settings_swipe_action_none">None</string>
+    <!-- Name of the swipe action to select/deselect a message -->
+    <string name="general_settings_swipe_action_toggle_selection">Toggle selection</string>
+    <!-- Name of the swipe action to mark as message as read/unread -->
+    <string name="general_settings_swipe_action_toggle_read">Mark as read/unread</string>
+    <!-- Name of the swipe action to add/remove a star -->
+    <string name="general_settings_swipe_action_toggle_star">Add/remove star</string>
+    <!-- Name of the swipe action to archive a message -->
+    <string name="general_settings_swipe_action_archive">Archive</string>
+    <!-- Name of the swipe action to delete a message -->
+    <string name="general_settings_swipe_action_delete">Delete</string>
+    <!-- Name of the swipe action to move a message to the spam folder -->
+    <string name="general_settings_swipe_action_spam">Spam</string>
+    <!-- Name of the swipe action to move a message to another folder -->
+    <string name="general_settings_swipe_action_move">Move</string>
+
     <string name="global_settings_privacy_hide_useragent">Hide mail client</string>
     <string name="global_settings_privacy_hide_useragent_detail">Remove K-9 User-Agent from mail headers</string>
     <string name="global_settings_privacy_hide_timezone">Hide timezone</string>
diff --git a/app/ui/legacy/src/main/res/values/themes.xml b/app/ui/legacy/src/main/res/values/themes.xml
index 9ad50f6d880..9488b7f8f20 100644
--- a/app/ui/legacy/src/main/res/values/themes.xml
+++ b/app/ui/legacy/src/main/res/values/themes.xml
@@ -30,7 +30,7 @@
         <item name="iconActionArchive">@drawable/ic_archive</item>
         <item name="iconActionCompose">@drawable/ic_pencil</item>
         <item name="iconActionDelete">@drawable/ic_trash_can</item>
-        <item name="iconActionMove">@drawable/ic_folder</item>
+        <item name="iconActionMove">@drawable/ic_move_to_folder</item>
         <item name="iconActionCopy">@drawable/ic_content_copy</item>
         <item name="iconActionNextStatus">@drawable/ic_chevron_right</item>
         <item name="iconActionRefresh">@drawable/ic_refresh</item>
@@ -91,6 +91,28 @@
         <item name="messageListAnswered">@drawable/ic_messagelist_answered</item>
         <item name="messageListForwarded">@drawable/ic_messagelist_forwarded</item>
         <item name="messageListAnsweredForwarded">@drawable/ic_messagelist_answered_forwarded</item>
+
+        <item name="messageListSwipeIconTint">#ffffff</item>
+        <item name="messageListSwipeDisabledBackgroundColor">@color/material_gray_200</item>
+        <item name="messageListSwipeSelectIcon">@drawable/ic_import_status</item>
+        <item name="messageListSwipeSelectBackgroundColor">@color/material_blue_600</item>
+        <item name="messageListSwipeMarkAsReadIcon">?attr/iconActionMarkAsRead</item>
+        <item name="messageListSwipeMarkAsReadBackgroundColor">@color/material_blue_600</item>
+        <item name="messageListSwipeMarkAsUnreadIcon">?attr/iconActionMarkAsUnread</item>
+        <item name="messageListSwipeMarkAsUnreadBackgroundColor">@color/material_blue_600</item>
+        <item name="messageListSwipeAddStarIcon">?attr/iconActionFlag</item>
+        <item name="messageListSwipeAddStarBackgroundColor">@color/material_orange_600</item>
+        <item name="messageListSwipeRemoveStarIcon">?attr/iconActionUnflag</item>
+        <item name="messageListSwipeRemoveStarBackgroundColor">@color/material_orange_600</item>
+        <item name="messageListSwipeArchiveIcon">?attr/iconActionArchive</item>
+        <item name="messageListSwipeArchiveBackgroundColor">@color/material_green_600</item>
+        <item name="messageListSwipeDeleteIcon">?attr/iconActionDelete</item>
+        <item name="messageListSwipeDeleteBackgroundColor">@color/material_red_600</item>
+        <item name="messageListSwipeSpamIcon">?attr/iconActionSpam</item>
+        <item name="messageListSwipeSpamBackgroundColor">@color/material_red_700</item>
+        <item name="messageListSwipeMoveIcon">?attr/iconActionMove</item>
+        <item name="messageListSwipeMoveBackgroundColor">@color/material_purple_500</item>
+
         <item name="messageStar">@drawable/btn_check_star</item>
         <item name="messageStarColor">#fbbc04</item>
         <item name="messageViewBackgroundColor">#ffffffff</item>
@@ -129,6 +151,9 @@
         <item name="behindRecyclerView">#F0F0F0</item>
     </style>
 
+    <style name="Theme.K9.Light" parent="Theme.K9.Light.Common" />
+
+    <style name="Theme.K9.Dark" parent="Theme.K9.Dark.Common" />
     <style name="Theme.K9.Dark.Common" parent="Theme.K9.Dark.Base">
         <item name="android:navigationBarColor">#000000</item>
         <item name="windowActionModeOverlay">true</item>
@@ -212,6 +237,28 @@
         <item name="messageListAnswered">@drawable/ic_messagelist_answered</item>
         <item name="messageListForwarded">@drawable/ic_messagelist_forwarded</item>
         <item name="messageListAnsweredForwarded">@drawable/ic_messagelist_answered_forwarded</item>
+
+        <item name="messageListSwipeIconTint">#ffffff</item>
+        <item name="messageListSwipeDisabledBackgroundColor">@color/material_gray_900</item>
+        <item name="messageListSwipeSelectIcon">@drawable/ic_import_status</item>
+        <item name="messageListSwipeSelectBackgroundColor">@color/material_blue_700</item>
+        <item name="messageListSwipeMarkAsReadIcon">?attr/iconActionMarkAsRead</item>
+        <item name="messageListSwipeMarkAsReadBackgroundColor">@color/material_blue_700</item>
+        <item name="messageListSwipeMarkAsUnreadIcon">?attr/iconActionMarkAsUnread</item>
+        <item name="messageListSwipeMarkAsUnreadBackgroundColor">@color/material_blue_700</item>
+        <item name="messageListSwipeAddStarIcon">?attr/iconActionFlag</item>
+        <item name="messageListSwipeAddStarBackgroundColor">@color/material_orange_700</item>
+        <item name="messageListSwipeRemoveStarIcon">?attr/iconActionUnflag</item>
+        <item name="messageListSwipeRemoveStarBackgroundColor">@color/material_orange_700</item>
+        <item name="messageListSwipeArchiveIcon">?attr/iconActionArchive</item>
+        <item name="messageListSwipeArchiveBackgroundColor">@color/material_green_700</item>
+        <item name="messageListSwipeDeleteIcon">?attr/iconActionDelete</item>
+        <item name="messageListSwipeDeleteBackgroundColor">@color/material_red_700</item>
+        <item name="messageListSwipeSpamIcon">?attr/iconActionSpam</item>
+        <item name="messageListSwipeSpamBackgroundColor">@color/material_red_800</item>
+        <item name="messageListSwipeMoveIcon">?attr/iconActionMove</item>
+        <item name="messageListSwipeMoveBackgroundColor">@color/material_purple_600</item>
+
         <item name="messageStar">@drawable/btn_check_star</item>
         <item name="messageStarColor">#fdd663</item>
         <item name="messageViewBackgroundColor">#000000</item>
@@ -251,9 +298,6 @@
         <item name="behindRecyclerView">#202020</item>
     </style>
 
-    <style name="Theme.K9.Light" parent="Theme.K9.Light.Common" />
-    <style name="Theme.K9.Dark" parent="Theme.K9.Dark.Common" />
-
     <style name="Theme.K9.Dialog.Light" parent="Theme.K9.Light">
         <item name="backgroundColorChooseAccountHeader">#cccccc</item>
     </style>
diff --git a/app/ui/legacy/src/main/res/xml/general_settings.xml b/app/ui/legacy/src/main/res/xml/general_settings.xml
index cf6933a72c2..ebc9be2bd3d 100644
--- a/app/ui/legacy/src/main/res/xml/general_settings.xml
+++ b/app/ui/legacy/src/main/res/xml/general_settings.xml
@@ -382,6 +382,29 @@
             android:summary="@string/global_settings_confirm_actions_summary"
             android:title="@string/global_settings_confirm_actions_title" />
 
+        <PreferenceScreen
+            android:key="swipe_actions"
+            android:title="@string/general_settings_swipe_actions_title"
+            search:ignore="true">
+
+            <ListPreference
+                android:dialogTitle="@string/general_settings_right_swipe_title"
+                android:entries="@array/swipe_action_names"
+                android:entryValues="@array/swipe_action_values"
+                android:key="swipe_action_right"
+                app:useSimpleSummaryProvider="true"
+                android:title="@string/general_settings_right_swipe_title" />
+
+            <ListPreference
+                android:dialogTitle="@string/general_settings_left_swipe_title"
+                android:entries="@array/swipe_action_names"
+                android:entryValues="@array/swipe_action_values"
+                android:key="swipe_action_left"
+                app:useSimpleSummaryProvider="true"
+                android:title="@string/general_settings_left_swipe_title" />
+
+        </PreferenceScreen>
+
     </PreferenceScreen>
 
     <PreferenceScreen


Test patch:
diff --git a/app/ui/legacy/src/test/java/com/fsck/k9/fragment/MessageListAdapterTest.kt b/app/ui/legacy/src/test/java/com/fsck/k9/ui/messagelist/MessageListAdapterTest.kt
similarity index 99%
rename from app/ui/legacy/src/test/java/com/fsck/k9/fragment/MessageListAdapterTest.kt
rename to app/ui/legacy/src/test/java/com/fsck/k9/ui/messagelist/MessageListAdapterTest.kt
index 095eb681595..c65312fddfc 100644
--- a/app/ui/legacy/src/test/java/com/fsck/k9/fragment/MessageListAdapterTest.kt
+++ b/app/ui/legacy/src/test/java/com/fsck/k9/ui/messagelist/MessageListAdapterTest.kt
@@ -1,4 +1,4 @@
-package com.fsck.k9.fragment
+package com.fsck.k9.ui.messagelist
 
 import android.content.Context
 import android.text.Spannable
@@ -23,8 +23,6 @@ import com.fsck.k9.mail.Address
 import com.fsck.k9.textString
 import com.fsck.k9.ui.R
 import com.fsck.k9.ui.helper.RelativeDateTimeFormatter
-import com.fsck.k9.ui.messagelist.MessageListAppearance
-import com.fsck.k9.ui.messagelist.MessageListItem
 import org.junit.Assert.assertEquals
 import org.junit.Assert.assertFalse
 import org.junit.Assert.assertNull