diff options
author | Johannes Thyssen Tishman <johannes@thyssentishman.com> | 2024-01-22 20:46:54 +0100 |
---|---|---|
committer | Robin Jarry <robin@jarry.cc> | 2024-01-25 23:33:01 +0100 |
commit | 40c25caafd583d4ee6ab3f5b318306e534abe480 (patch) | |
tree | 6d3b63da9c8bf409b1e16fa8af1e456e43b7e1b1 /commands | |
parent | e4eab644b0ee1a7bc87fa0581cf0ac28eb64bf58 (diff) | |
download | aerc-40c25caafd583d4ee6ab3f5b318306e534abe480.tar.gz |
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>
Diffstat (limited to 'commands')
-rw-r--r-- | commands/msg/archive.go | 4 | ||||
-rw-r--r-- | commands/msg/move.go | 171 |
2 files changed, 148 insertions, 27 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: |