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) } } }