mirror of https://git.sr.ht/~rjarry/aerc
You cannot select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
268 lines
6.2 KiB
Go
268 lines
6.2 KiB
Go
package msg
|
|
|
|
import (
|
|
"bytes"
|
|
"fmt"
|
|
"time"
|
|
|
|
"git.sr.ht/~rjarry/aerc/app"
|
|
"git.sr.ht/~rjarry/aerc/commands"
|
|
"git.sr.ht/~rjarry/aerc/config"
|
|
"git.sr.ht/~rjarry/aerc/lib"
|
|
"git.sr.ht/~rjarry/aerc/lib/log"
|
|
"git.sr.ht/~rjarry/aerc/lib/marker"
|
|
"git.sr.ht/~rjarry/aerc/lib/ui"
|
|
"git.sr.ht/~rjarry/aerc/models"
|
|
"git.sr.ht/~rjarry/aerc/worker/types"
|
|
)
|
|
|
|
type Move struct {
|
|
CreateFolders bool `opt:"-p" desc:"Create missing folders if required."`
|
|
Account string `opt:"-a" complete:"CompleteAccount" desc:"Move to specified account."`
|
|
MultiFileStrategy *types.MultiFileStrategy `opt:"-m" action:"ParseMFS" complete:"CompleteMFS" desc:"Multi-file strategy."`
|
|
Folder string `opt:"folder" complete:"CompleteFolder" desc:"Target folder."`
|
|
}
|
|
|
|
func init() {
|
|
commands.Register(Move{})
|
|
}
|
|
|
|
func (Move) Description() string {
|
|
return "Move the selected message(s) to the specified folder."
|
|
}
|
|
|
|
func (Move) Context() commands.CommandContext {
|
|
return commands.MESSAGE_LIST | commands.MESSAGE_VIEWER
|
|
}
|
|
|
|
func (Move) Aliases() []string {
|
|
return []string{"mv", "move"}
|
|
}
|
|
|
|
func (m *Move) ParseMFS(arg string) error {
|
|
if arg != "" {
|
|
mfs, ok := types.StrToStrategy[arg]
|
|
if !ok {
|
|
return fmt.Errorf("invalid multi-file strategy %s", arg)
|
|
}
|
|
m.MultiFileStrategy = &mfs
|
|
}
|
|
return nil
|
|
}
|
|
|
|
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 (Move) CompleteMFS(arg string) []string {
|
|
return commands.FilterList(types.StrategyStrs(), arg, nil)
|
|
}
|
|
|
|
func (m Move) Execute(args []string) error {
|
|
h := newHelper()
|
|
acct, err := h.account()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
store, err := h.store()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
uids, err := h.markedOrSelectedUids()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
next := findNextNonDeleted(uids, store)
|
|
marker := store.Marker()
|
|
marker.ClearVisualMark()
|
|
|
|
if len(m.Account) == 0 {
|
|
store.Move(uids, m.Folder, m.CreateFolders, m.MultiFileStrategy,
|
|
func(msg types.WorkerMessage) {
|
|
m.CallBack(msg, acct, uids, next, marker, 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 []models.UID
|
|
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 {
|
|
mfs := types.Refuse
|
|
store.Delete(appended, &mfs, func(msg types.WorkerMessage) {
|
|
m.CallBack(msg, acct, appended, next, marker, timeout)
|
|
})
|
|
}
|
|
}()
|
|
return nil
|
|
}
|
|
|
|
func (m Move) CallBack(
|
|
msg types.WorkerMessage,
|
|
acct *app.AccountView,
|
|
uids []models.UID,
|
|
next *models.MessageInfo,
|
|
marker marker.Marker,
|
|
timeout bool,
|
|
) {
|
|
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"
|
|
}
|
|
dest := m.Folder
|
|
if len(m.Account) > 0 {
|
|
dest = fmt.Sprintf("%s in %s", m.Folder, m.Account)
|
|
}
|
|
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)
|
|
}
|
|
if store := acct.Store(); store != nil {
|
|
handleDone(acct, next, store)
|
|
}
|
|
case *types.Error:
|
|
app.PushError(msg.Error.Error())
|
|
marker.Remark()
|
|
case *types.Unsupported:
|
|
marker.Remark()
|
|
app.PushError("error, unsupported for this worker")
|
|
}
|
|
}
|
|
|
|
func handleDone(
|
|
acct *app.AccountView,
|
|
next *models.MessageInfo,
|
|
store *lib.MessageStore,
|
|
) {
|
|
h := newHelper()
|
|
mv, isMsgView := h.msgProvider.(*app.MessageViewer)
|
|
switch {
|
|
case isMsgView && !config.Ui.NextMessageOnDelete:
|
|
app.RemoveTab(h.msgProvider, true)
|
|
case isMsgView:
|
|
if next == nil {
|
|
app.RemoveTab(h.msgProvider, true)
|
|
acct.Messages().Select(-1)
|
|
ui.Invalidate()
|
|
return
|
|
}
|
|
lib.NewMessageStoreView(next, mv.MessageView().SeenFlagSet(),
|
|
store, app.CryptoProvider(), app.DecryptKeys,
|
|
func(view lib.MessageView, err error) {
|
|
if err != nil {
|
|
app.PushError(err.Error())
|
|
return
|
|
}
|
|
nextMv, err := app.NewMessageViewer(acct, view)
|
|
if err != nil {
|
|
app.PushError(err.Error())
|
|
return
|
|
}
|
|
app.ReplaceTab(mv, nextMv, next.Envelope.Subject, true)
|
|
})
|
|
default:
|
|
if next == nil {
|
|
// We moved the last message, select the new last message
|
|
// instead of the first message
|
|
acct.Messages().Select(-1)
|
|
}
|
|
}
|
|
}
|