mirror of
https://git.sr.ht/~rjarry/aerc
synced 2025-02-22 23:23:57 +01:00

A single notmuch message can represent multiple files. As a result, file-based operations like move, copy, and delete can be ambiguous. Add a new account config option, multi-file-strategy, to tell aerc how to handle these ambiguous cases. Also add options to relevant commands to set the multi-file strategy on a per-invocation basis. If no multi-file strategy is set, refuse to take file-based actions on multi-file messages. This default behavior is mostly the same as aerc's previous behavior, but a bit stricter in some cases which previously tried to be smart about multi-file operations (e.g., move and delete). Applying multi-file strategies to cross-account copy and move operations is not implemented. These operations will proceed as they have in the past -- aerc will copy/move a single file. However, for cross-account move operations, aerc will refuse to delete multiple files to prevent data loss as not all of the files are added to the destination account. See the changes to aerc-notmuch(5) for details on the currently supported multi-file strategies. Changelog-added: Tell aerc how to handle file-based operations on multi-file notmuch messages with the account config option `multi-file-strategy` and the `-m` flag to `:archive`, `:copy`, `:delete`, and `:move`. Signed-off-by: Jason Cox <me@jasoncarloscox.com> Tested-by: Maarten Aertsen <maarten@nlnetlabs.nl> Acked-by: Robin Jarry <robin@jarry.cc>
264 lines
5.8 KiB
Go
264 lines
5.8 KiB
Go
//go:build notmuch
|
|
// +build notmuch
|
|
|
|
package notmuch
|
|
|
|
import (
|
|
"testing"
|
|
|
|
"git.sr.ht/~rjarry/aerc/worker/types"
|
|
"github.com/emersion/go-maildir"
|
|
)
|
|
|
|
func TestFilterForStrategy(t *testing.T) {
|
|
tests := []struct {
|
|
filenames []string
|
|
strategy types.MultiFileStrategy
|
|
curDir string
|
|
expectedAct []string
|
|
expectedDel []string
|
|
expectedErr bool
|
|
}{
|
|
// if there's only one file, always act on it
|
|
{
|
|
filenames: []string{"/h/j/m/A/cur/a.b.c:2,"},
|
|
strategy: types.Refuse,
|
|
curDir: "/h/j/m/B",
|
|
expectedAct: []string{"/h/j/m/A/cur/a.b.c:2,"},
|
|
expectedDel: []string{},
|
|
},
|
|
{
|
|
filenames: []string{"/h/j/m/A/cur/a.b.c:2,"},
|
|
strategy: types.ActAll,
|
|
curDir: "/h/j/m/B",
|
|
expectedAct: []string{"/h/j/m/A/cur/a.b.c:2,"},
|
|
expectedDel: []string{},
|
|
},
|
|
{
|
|
filenames: []string{"/h/j/m/A/cur/a.b.c:2,"},
|
|
strategy: types.ActOne,
|
|
curDir: "/h/j/m/B",
|
|
expectedAct: []string{"/h/j/m/A/cur/a.b.c:2,"},
|
|
expectedDel: []string{},
|
|
},
|
|
{
|
|
filenames: []string{"/h/j/m/A/cur/a.b.c:2,"},
|
|
strategy: types.ActOneDelRest,
|
|
curDir: "/h/j/m/B",
|
|
expectedAct: []string{"/h/j/m/A/cur/a.b.c:2,"},
|
|
expectedDel: []string{},
|
|
},
|
|
{
|
|
filenames: []string{"/h/j/m/A/cur/a.b.c:2,"},
|
|
strategy: types.ActDir,
|
|
curDir: "/h/j/m/B",
|
|
expectedAct: []string{"/h/j/m/A/cur/a.b.c:2,"},
|
|
expectedDel: []string{},
|
|
},
|
|
{
|
|
filenames: []string{"/h/j/m/A/cur/a.b.c:2,"},
|
|
strategy: types.ActDirDelRest,
|
|
curDir: "/h/j/m/B",
|
|
expectedAct: []string{"/h/j/m/A/cur/a.b.c:2,"},
|
|
expectedDel: []string{},
|
|
},
|
|
|
|
// follow strategy for multiple files
|
|
{
|
|
filenames: []string{
|
|
"/h/j/m/A/cur/a.b.c:2,",
|
|
"/h/j/m/B/new/b.c.d",
|
|
"/h/j/m/B/cur/c.d.e:2,S",
|
|
"/h/j/m/C/new/d.e.f",
|
|
},
|
|
strategy: types.Refuse,
|
|
curDir: "/h/j/m/B",
|
|
expectedErr: true,
|
|
},
|
|
{
|
|
filenames: []string{
|
|
"/h/j/m/A/cur/a.b.c:2,",
|
|
"/h/j/m/B/new/b.c.d",
|
|
"/h/j/m/B/cur/c.d.e:2,S",
|
|
"/h/j/m/C/new/d.e.f",
|
|
},
|
|
strategy: types.ActAll,
|
|
curDir: "/h/j/m/B",
|
|
expectedAct: []string{
|
|
"/h/j/m/A/cur/a.b.c:2,",
|
|
"/h/j/m/B/new/b.c.d",
|
|
"/h/j/m/B/cur/c.d.e:2,S",
|
|
"/h/j/m/C/new/d.e.f",
|
|
},
|
|
expectedDel: []string{},
|
|
},
|
|
{
|
|
filenames: []string{
|
|
"/h/j/m/A/cur/a.b.c:2,",
|
|
"/h/j/m/B/new/b.c.d",
|
|
"/h/j/m/B/cur/c.d.e:2,S",
|
|
"/h/j/m/C/new/d.e.f",
|
|
},
|
|
strategy: types.ActOne,
|
|
curDir: "/h/j/m/B",
|
|
expectedAct: []string{"/h/j/m/A/cur/a.b.c:2,"},
|
|
expectedDel: []string{},
|
|
},
|
|
{
|
|
filenames: []string{
|
|
"/h/j/m/A/cur/a.b.c:2,",
|
|
"/h/j/m/B/new/b.c.d",
|
|
"/h/j/m/B/cur/c.d.e:2,S",
|
|
"/h/j/m/C/new/d.e.f",
|
|
},
|
|
strategy: types.ActOneDelRest,
|
|
curDir: "/h/j/m/B",
|
|
expectedAct: []string{"/h/j/m/A/cur/a.b.c:2,"},
|
|
expectedDel: []string{
|
|
"/h/j/m/B/new/b.c.d",
|
|
"/h/j/m/B/cur/c.d.e:2,S",
|
|
"/h/j/m/C/new/d.e.f",
|
|
},
|
|
},
|
|
{
|
|
filenames: []string{
|
|
"/h/j/m/A/cur/a.b.c:2,",
|
|
"/h/j/m/B/new/b.c.d",
|
|
"/h/j/m/B/cur/c.d.e:2,S",
|
|
"/h/j/m/C/new/d.e.f",
|
|
},
|
|
strategy: types.ActDir,
|
|
curDir: "/h/j/m/B",
|
|
expectedAct: []string{
|
|
"/h/j/m/B/new/b.c.d",
|
|
"/h/j/m/B/cur/c.d.e:2,S",
|
|
},
|
|
expectedDel: []string{},
|
|
},
|
|
{
|
|
filenames: []string{
|
|
"/h/j/m/A/cur/a.b.c:2,",
|
|
"/h/j/m/B/new/b.c.d",
|
|
"/h/j/m/B/cur/c.d.e:2,S",
|
|
"/h/j/m/C/new/d.e.f",
|
|
},
|
|
strategy: types.ActDirDelRest,
|
|
curDir: "/h/j/m/B",
|
|
expectedAct: []string{
|
|
"/h/j/m/B/new/b.c.d",
|
|
"/h/j/m/B/cur/c.d.e:2,S",
|
|
},
|
|
expectedDel: []string{
|
|
"/h/j/m/A/cur/a.b.c:2,",
|
|
"/h/j/m/C/new/d.e.f",
|
|
},
|
|
},
|
|
|
|
// refuse to act on multiple files for ActDir and friends if
|
|
// no current dir is provided
|
|
{
|
|
filenames: []string{
|
|
"/h/j/m/A/cur/a.b.c:2,",
|
|
"/h/j/m/B/new/b.c.d",
|
|
"/h/j/m/B/cur/c.d.e:2,S",
|
|
"/h/j/m/C/new/d.e.f",
|
|
},
|
|
strategy: types.ActDir,
|
|
curDir: "",
|
|
expectedErr: true,
|
|
},
|
|
{
|
|
filenames: []string{
|
|
"/h/j/m/A/cur/a.b.c:2,",
|
|
"/h/j/m/B/new/b.c.d",
|
|
"/h/j/m/B/cur/c.d.e:2,S",
|
|
"/h/j/m/C/new/d.e.f",
|
|
},
|
|
strategy: types.ActDirDelRest,
|
|
curDir: "",
|
|
expectedErr: true,
|
|
},
|
|
|
|
// act on multiple files w/o current dir for other strategies
|
|
{
|
|
filenames: []string{
|
|
"/h/j/m/A/cur/a.b.c:2,",
|
|
"/h/j/m/B/new/b.c.d",
|
|
"/h/j/m/B/cur/c.d.e:2,S",
|
|
"/h/j/m/C/new/d.e.f",
|
|
},
|
|
strategy: types.ActAll,
|
|
curDir: "",
|
|
expectedAct: []string{
|
|
"/h/j/m/A/cur/a.b.c:2,",
|
|
"/h/j/m/B/new/b.c.d",
|
|
"/h/j/m/B/cur/c.d.e:2,S",
|
|
"/h/j/m/C/new/d.e.f",
|
|
},
|
|
expectedDel: []string{},
|
|
},
|
|
{
|
|
filenames: []string{
|
|
"/h/j/m/A/cur/a.b.c:2,",
|
|
"/h/j/m/B/new/b.c.d",
|
|
"/h/j/m/B/cur/c.d.e:2,S",
|
|
"/h/j/m/C/new/d.e.f",
|
|
},
|
|
strategy: types.ActOne,
|
|
curDir: "",
|
|
expectedAct: []string{"/h/j/m/A/cur/a.b.c:2,"},
|
|
expectedDel: []string{},
|
|
},
|
|
{
|
|
filenames: []string{
|
|
"/h/j/m/A/cur/a.b.c:2,",
|
|
"/h/j/m/B/new/b.c.d",
|
|
"/h/j/m/B/cur/c.d.e:2,S",
|
|
"/h/j/m/C/new/d.e.f",
|
|
},
|
|
strategy: types.ActOneDelRest,
|
|
curDir: "",
|
|
expectedAct: []string{"/h/j/m/A/cur/a.b.c:2,"},
|
|
expectedDel: []string{
|
|
"/h/j/m/B/new/b.c.d",
|
|
"/h/j/m/B/cur/c.d.e:2,S",
|
|
"/h/j/m/C/new/d.e.f",
|
|
},
|
|
},
|
|
}
|
|
|
|
for i, test := range tests {
|
|
act, del, err := filterForStrategy(test.filenames, test.strategy,
|
|
maildir.Dir(test.curDir))
|
|
|
|
if test.expectedErr && err == nil {
|
|
t.Errorf("[test %d] got nil, expected error", i)
|
|
}
|
|
|
|
if !test.expectedErr && err != nil {
|
|
t.Errorf("[test %d] got %v, expected nil", i, err)
|
|
}
|
|
|
|
if !arrEq(act, test.expectedAct) {
|
|
t.Errorf("[test %d] got %v, expected %v", i, act, test.expectedAct)
|
|
}
|
|
|
|
if !arrEq(del, test.expectedDel) {
|
|
t.Errorf("[test %d] got %v, expected %v", i, del, test.expectedDel)
|
|
}
|
|
}
|
|
}
|
|
|
|
func arrEq(a, b []string) bool {
|
|
if len(a) != len(b) {
|
|
return false
|
|
}
|
|
|
|
for i := range a {
|
|
if a[i] != b[i] {
|
|
return false
|
|
}
|
|
}
|
|
|
|
return true
|
|
}
|