mirror of
https://git.sr.ht/~rjarry/aerc
synced 2026-03-06 18:23:44 +01:00
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>
This commit is contained in:
committed by
Robin Jarry
parent
e4eab644b0
commit
40c25caafd
@@ -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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
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)
|
||||
|
||||
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()
|
||||
}
|
||||
})
|
||||
dest := m.Folder
|
||||
if len(m.Account) > 0 {
|
||||
dest = fmt.Sprintf("%s in %s", m.Folder, m.Account)
|
||||
}
|
||||
|
||||
return nil
|
||||
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:
|
||||
|
||||
@@ -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.
|
||||
|
||||
Reference in New Issue
Block a user