summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorJohannes Thyssen Tishman <johannes@thyssentishman.com>2024-01-22 20:46:54 +0100
committerRobin Jarry <robin@jarry.cc>2024-01-25 23:33:01 +0100
commit40c25caafd583d4ee6ab3f5b318306e534abe480 (patch)
tree6d3b63da9c8bf409b1e16fa8af1e456e43b7e1b1
parente4eab644b0ee1a7bc87fa0581cf0ac28eb64bf58 (diff)
downloadaerc-40c25caafd583d4ee6ab3f5b318306e534abe480.tar.gz
aerc-40c25caafd583d4ee6ab3f5b318306e534abe480.zip
mv: allow to move messages across accounts
Add a new -a flag to :mv. When specified, an account name is required before the folder name. If the destination folder doesn't exist, it will be created whether or not the -p flag is specified. Changelog-added: Move messages across accounts with `:mv -a <account> <folder>`. Signed-off-by: Johannes Thyssen Tishman <johannes@thyssentishman.com> Acked-by: Robin Jarry <robin@jarry.cc>
-rw-r--r--commands/msg/archive.go4
-rw-r--r--commands/msg/move.go171
-rw-r--r--doc/aerc.1.scd11
3 files changed, 155 insertions, 31 deletions
diff --git a/commands/msg/archive.go b/commands/msg/archive.go
index 13f53290..49f375a8 100644
--- a/commands/msg/archive.go
+++ b/commands/msg/archive.go
@@ -4,6 +4,7 @@ import (
"fmt"
"strings"
"sync"
+ "time"
"git.sr.ht/~rjarry/aerc/app"
"git.sr.ht/~rjarry/aerc/commands"
@@ -136,7 +137,8 @@ func archive(msgs []*models.MessageInfo, archiveType string) error {
} else {
s = "%d message archived to %s"
}
- handleDone(acct, next, fmt.Sprintf(s, len(uids), archiveDir), store)
+ app.PushStatus(fmt.Sprintf(s, len(uids), archiveDir), 10*time.Second)
+ handleDone(acct, next, store)
}
}()
return nil
diff --git a/commands/msg/move.go b/commands/msg/move.go
index 000e2b2a..defe94d1 100644
--- a/commands/msg/move.go
+++ b/commands/msg/move.go
@@ -1,6 +1,7 @@
package msg
import (
+ "bytes"
"fmt"
"time"
@@ -9,12 +10,14 @@ import (
"git.sr.ht/~rjarry/aerc/config"
"git.sr.ht/~rjarry/aerc/lib"
"git.sr.ht/~rjarry/aerc/lib/ui"
+ "git.sr.ht/~rjarry/aerc/log"
"git.sr.ht/~rjarry/aerc/models"
"git.sr.ht/~rjarry/aerc/worker/types"
)
type Move struct {
CreateFolders bool `opt:"-p"`
+ Account string `opt:"-a" complete:"CompleteAccount"`
Folder string `opt:"folder" complete:"CompleteFolder"`
}
@@ -30,8 +33,21 @@ func (Move) Aliases() []string {
return []string{"mv", "move"}
}
-func (*Move) CompleteFolder(arg string) []string {
- return commands.GetFolders(arg)
+func (*Move) CompleteAccount(arg string) []string {
+ return commands.FilterList(app.AccountNames(), arg, commands.QuoteSpace)
+}
+
+func (m *Move) CompleteFolder(arg string) []string {
+ var acct *app.AccountView
+ if len(m.Account) > 0 {
+ acct, _ = app.Account(m.Account)
+ } else {
+ acct = app.SelectedAccount()
+ }
+ if acct == nil {
+ return nil
+ }
+ return commands.FilterList(acct.Directories().List(), arg, nil)
}
func (m Move) Execute(args []string) error {
@@ -44,47 +60,150 @@ func (m Move) Execute(args []string) error {
if err != nil {
return err
}
- msgs, err := h.messages()
+ uids, err := h.markedOrSelectedUids()
if err != nil {
return err
}
- var uids []uint32
- for _, msg := range msgs {
- uids = append(uids, msg.Uid)
+
+ if len(m.Account) == 0 {
+ store.Move(uids, m.Folder, m.CreateFolders, func(msg types.WorkerMessage) {
+ m.CallBack(msg, acct, uids, false)
+ })
+ return nil
}
- marker := store.Marker()
- marker.ClearVisualMark()
- next := findNextNonDeleted(uids, store)
- store.Move(uids, m.Folder, m.CreateFolders, func(
- msg types.WorkerMessage,
- ) {
- switch msg := msg.(type) {
- case *types.Done:
- var s string
- if len(uids) > 1 {
- s = "%d messages moved to %s"
- } else {
- s = "%d message moved to %s"
- }
- handleDone(acct, next, fmt.Sprintf(s, len(uids), m.Folder), store)
- case *types.Error:
- app.PushError(msg.Error.Error())
- marker.Remark()
+ destAcct, err := app.Account(m.Account)
+ if err != nil {
+ return err
+ }
+
+ destStore := destAcct.Store()
+ if destStore == nil {
+ app.PushError(fmt.Sprintf("No message store in %s", m.Account))
+ return nil
+ }
+
+ var messages []*types.FullMessage
+ fetchDone := make(chan bool, 1)
+ store.FetchFull(uids, func(fm *types.FullMessage) {
+ messages = append(messages, fm)
+ if len(messages) == len(uids) {
+ fetchDone <- true
}
})
+ // Since this operation can take some time with some backends
+ // (e.g. IMAP), provide some feedback to inform the user that
+ // something is happening
+ app.PushStatus("Moving messages...", 10*time.Second)
+
+ var appended []uint32
+ var timeout bool
+ go func() {
+ defer log.PanicHandler()
+
+ select {
+ case <-fetchDone:
+ break
+ case <-time.After(30 * time.Second):
+ // TODO: find a better way to determine if store.FetchFull()
+ // has finished with some errors.
+ app.PushError("Failed to fetch all messages")
+ if len(messages) == 0 {
+ return
+ }
+ }
+
+ AppendLoop:
+ for _, fm := range messages {
+ done := make(chan bool, 1)
+ uid := fm.Content.Uid
+ buf := new(bytes.Buffer)
+ _, err = buf.ReadFrom(fm.Content.Reader)
+ if err != nil {
+ log.Errorf("could not get reader for uid %d", uid)
+ break
+ }
+ destStore.Append(
+ m.Folder,
+ models.SeenFlag,
+ time.Now(),
+ buf,
+ buf.Len(),
+ func(msg types.WorkerMessage) {
+ switch msg := msg.(type) {
+ case *types.Done:
+ appended = append(appended, uid)
+ done <- true
+ case *types.Error:
+ log.Errorf("AppendMessage failed: %v", msg.Error)
+ done <- false
+ }
+ },
+ )
+ select {
+ case ok := <-done:
+ if !ok {
+ break AppendLoop
+ }
+ case <-time.After(30 * time.Second):
+ log.Warnf("timed-out: appended %d of %d", len(appended), len(messages))
+ timeout = true
+ break AppendLoop
+ }
+ }
+ if len(appended) > 0 {
+ store.Delete(appended, func(msg types.WorkerMessage) {
+ m.CallBack(msg, acct, appended, timeout)
+ })
+ }
+ }()
return nil
}
+func (m Move) CallBack(msg types.WorkerMessage, acct *app.AccountView, uids []uint32, timeout bool) {
+ store := acct.Store()
+ sel := store.Selected()
+ marker := store.Marker()
+ marker.ClearVisualMark()
+ next := findNextNonDeleted(uids, store)
+
+ dest := m.Folder
+ if len(m.Account) > 0 {
+ dest = fmt.Sprintf("%s in %s", m.Folder, m.Account)
+ }
+
+ switch msg := msg.(type) {
+ case *types.Done:
+ var s string
+ if len(uids) > 1 {
+ s = "%d messages moved to %s"
+ } else {
+ s = "%d message moved to %s"
+ }
+ if timeout {
+ s = "timed-out: only " + s
+ app.PushError(fmt.Sprintf(s, len(uids), dest))
+ } else {
+ app.PushStatus(fmt.Sprintf(s, len(uids), dest), 10*time.Second)
+ }
+ handleDone(acct, next, store)
+ case *types.Error:
+ app.PushError(msg.Error.Error())
+ marker.Remark()
+ case *types.Unsupported:
+ marker.Remark()
+ store.Select(sel.Uid)
+ app.PushError("error, unsupported for this worker")
+ }
+}
+
func handleDone(
acct *app.AccountView,
next *models.MessageInfo,
- message string,
store *lib.MessageStore,
) {
h := newHelper()
- app.PushStatus(message, 10*time.Second)
mv, isMsgView := h.msgProvider.(*app.MessageViewer)
switch {
case isMsgView && !config.Ui.NextMessageOnDelete:
diff --git a/doc/aerc.1.scd b/doc/aerc.1.scd
index c7fc8542..d77bbf03 100644
--- a/doc/aerc.1.scd
+++ b/doc/aerc.1.scd
@@ -335,11 +335,14 @@ message list, the message in the message viewer, etc).
*-E*: Forces *[compose].edit-headers* = _false_ for this message only.
-*:move* [*-p*] _<target>_++
-*:mv* [*-p*] _<target>_
- Moves the selected message(s) to the target folder.
+*:move* [*-p*] [*-a* _<account>_] _<folder>_++
+*:mv* [*-p*] [*-a* _<account>_] _<folder>_
+ Moves the selected message(s) to _<folder>_.
- *-p*: Create the _<target>_ folder if it does not exist.
+ *-p*: Create _<folder>_ if it does not exist.
+
+ *-a*: Move to _<folder>_ of _<account>_. If _<folder>_ does
+ not exist, it will be created whether or not *-p* is used.
*:patch* _<args ...>_
Patch management sub-commands. See *aerc-patch*(7) for more details.