1
0
Fork 0
mirror of https://git.sr.ht/~rjarry/aerc synced 2026-04-25 16:08:23 +02:00
aerc/commands/msg/archive.go
Bence Ferdinandy cb6d302cca archive: pass account and store to archive function
The archive() function calls newHelper() which relies on
app.SelectedTabContent() to determine the current context. When called
from an OnClose callback after sending a reply with archive-on-reply
configured, the composer tab is already closed and the user may have
switched accounts, causing a crash.

Pass the account and store directly to archive() from the calling
context instead of querying the current UI state.

Changelog-fixed: Crash when switching accounts quickly after sending
 a reply with archive-on-reply enabled.
Signed-off-by: Bence Ferdinandy <bence@ferdinandy.com>
Acked-by: Robin Jarry <robin@jarry.cc>
2026-02-09 14:50:56 +01:00

176 lines
4 KiB
Go

package msg
import (
"fmt"
"slices"
"strings"
"sync"
"time"
"git.sr.ht/~rjarry/aerc/app"
"git.sr.ht/~rjarry/aerc/commands"
"git.sr.ht/~rjarry/aerc/lib"
"git.sr.ht/~rjarry/aerc/lib/log"
"git.sr.ht/~rjarry/aerc/models"
"git.sr.ht/~rjarry/aerc/worker/types"
)
const (
ARCHIVE_FLAT = "flat"
ARCHIVE_YEAR = "year"
ARCHIVE_MONTH = "month"
)
var ARCHIVE_TYPES = []string{ARCHIVE_FLAT, ARCHIVE_YEAR, ARCHIVE_MONTH}
type Archive struct {
MultiFileStrategy *types.MultiFileStrategy `opt:"-m" action:"ParseMFS" complete:"CompleteMFS" desc:"Multi-file strategy."`
Type string `opt:"type" action:"ParseArchiveType" metavar:"flat|year|month" complete:"CompleteType" desc:"Archiving scheme."`
}
func (a *Archive) ParseMFS(arg string) error {
if arg != "" {
mfs, ok := types.StrToStrategy[arg]
if !ok {
return fmt.Errorf("invalid multi-file strategy %s", arg)
}
a.MultiFileStrategy = &mfs
}
return nil
}
func (a *Archive) ParseArchiveType(arg string) error {
if slices.Contains(ARCHIVE_TYPES, arg) {
a.Type = arg
return nil
}
return fmt.Errorf("invalid archive type")
}
func init() {
commands.Register(Archive{})
}
func (Archive) Description() string {
return "Move the selected message to the archive."
}
func (Archive) Context() commands.CommandContext {
return commands.MESSAGE_LIST | commands.MESSAGE_VIEWER
}
func (Archive) Aliases() []string {
return []string{"archive"}
}
func (Archive) CompleteMFS(arg string) []string {
return commands.FilterList(types.StrategyStrs(), arg, nil)
}
func (*Archive) CompleteType(arg string) []string {
return commands.FilterList(ARCHIVE_TYPES, arg, nil)
}
func (a Archive) Execute(args []string) error {
h := newHelper()
msgs, err := h.messages()
if err != nil {
return err
}
acct, err := h.account()
if err != nil {
return err
}
store, err := h.store()
if err != nil {
return err
}
return archive(msgs, a.MultiFileStrategy, a.Type, acct, store)
}
func archive(msgs []*models.MessageInfo, mfs *types.MultiFileStrategy,
archiveType string, acct *app.AccountView, store *lib.MessageStore,
) error {
var uids []models.UID
for _, msg := range msgs {
uids = append(uids, msg.Uid)
}
archiveDir := acct.AccountConfig().Archive
marker := store.Marker()
marker.ClearVisualMark()
next := findNextNonDeleted(uids, store)
var uidMap map[string][]models.UID
switch archiveType {
case ARCHIVE_MONTH:
uidMap = groupBy(msgs, func(msg *models.MessageInfo) string {
dir := strings.Join([]string{
archiveDir,
fmt.Sprintf("%d", msg.Envelope.Date.Year()),
fmt.Sprintf("%02d", msg.Envelope.Date.Month()),
}, acct.Worker().PathSeparator(),
)
return dir
})
case ARCHIVE_YEAR:
uidMap = groupBy(msgs, func(msg *models.MessageInfo) string {
dir := strings.Join([]string{
archiveDir,
fmt.Sprintf("%v", msg.Envelope.Date.Year()),
}, acct.Worker().PathSeparator(),
)
return dir
})
case ARCHIVE_FLAT:
uidMap = make(map[string][]models.UID)
uidMap[archiveDir] = commands.UidsFromMessageInfos(msgs)
}
var wg sync.WaitGroup
wg.Add(len(uidMap))
success := true
for dir, uids := range uidMap {
store.Move(uids, dir, true, mfs, func(
msg types.WorkerMessage,
) {
switch msg := msg.(type) {
case *types.Done:
wg.Done()
case *types.Error:
app.PushError(msg.Error.Error())
success = false
wg.Done()
marker.Remark()
}
})
}
// we need to do that in the background, else we block the main thread
go func() {
defer log.PanicHandler()
wg.Wait()
if success {
var s string
if len(uids) > 1 {
s = "%d messages archived to %s"
} else {
s = "%d message archived to %s"
}
app.PushStatus(fmt.Sprintf(s, len(uids), archiveDir), 10*time.Second)
handleDone(acct, next, store)
}
}()
return nil
}
func groupBy(msgs []*models.MessageInfo,
grouper func(*models.MessageInfo) string,
) map[string][]models.UID {
m := make(map[string][]models.UID)
for _, msg := range msgs {
group := grouper(msg)
m[group] = append(m[group], msg.Uid)
}
return m
}