Add support for :rmdir

The `:rmdir` command removes the current directory (`-f` is required if
the directory is not empty).

This is not supported on the notmuch backend.

An issue with the maildir backend is that some sync programs (e.g.
offlineimap) may recover the directory after it is deleted.  They need
to specifically be configured to accept deletions, or special commands
need to be executed (e.g. `offlineimap --delete-folder`) to properly
delete folders.

A danger of using this on the IMAP backend is that it is possible for a
new message to be added to the directory and for aerc to not show it
immediately (due to a slow connection) - using `:rmdir` at this moment
(with `-f` if the directory already contains messages) would delete the
directory and the new message that just arrived (and all other
contents).  This is documented in aerc(1) so that users are aware of
possible risks.
This commit is contained in:
ARaspiK 2020-08-18 20:27:23 +00:00 committed by Reto Brunner
parent f4dc7e1f74
commit fe1cabb077
8 changed files with 161 additions and 0 deletions

96
commands/account/rmdir.go Normal file
View file

@ -0,0 +1,96 @@
package account
import (
"errors"
"time"
"git.sr.ht/~sircmpwn/getopt"
"git.sr.ht/~sircmpwn/aerc/widgets"
"git.sr.ht/~sircmpwn/aerc/worker/types"
)
type RemoveDir struct{}
func init() {
register(RemoveDir{})
}
func (RemoveDir) Aliases() []string {
return []string{"rmdir"}
}
func (RemoveDir) Complete(aerc *widgets.Aerc, args []string) []string {
return nil
}
func (RemoveDir) Execute(aerc *widgets.Aerc, args []string) error {
acct := aerc.SelectedAccount()
if acct == nil {
return errors.New("No account selected")
}
force := false
opts, optind, err := getopt.Getopts(args, "f")
if err != nil {
return err
}
for _, opt := range opts {
switch opt.Option {
case 'f':
force = true
}
}
if len(args) != optind {
return errors.New("Usage: rmdir [-f]")
}
// Check for any messages in the directory.
if !acct.Messages().Empty() && !force {
return errors.New("Refusing to remove non-empty directory; use -f")
}
curDir := acct.SelectedDirectory()
var newDir string
dirFound := false
if oldDir, ok := history[acct.Name()]; ok {
if oldDir != curDir {
newDir = oldDir
dirFound = true
}
}
if !dirFound {
for _, dir := range acct.Directories().List() {
if dir != curDir {
newDir = dir
dirFound = true
break
}
}
}
if !dirFound {
return errors.New("No directory to move to afterwards!")
}
acct.Directories().Select(newDir)
acct.Worker().PostAction(&types.RemoveDirectory{
Directory: curDir,
}, func(msg types.WorkerMessage) {
switch msg := msg.(type) {
case *types.Done:
aerc.PushStatus("Directory removed.", 10*time.Second)
case *types.Error:
aerc.PushError(" " + msg.Error.Error())
case *types.Unsupported:
aerc.PushError(":rmdir is not supported by the backend.")
}
})
return nil
}

View file

@ -214,6 +214,28 @@ message list, the message in the message viewer, etc).
*mkdir* <name> *mkdir* <name>
Creates a new folder for this account and changes to that folder. Creates a new folder for this account and changes to that folder.
This is not supported on the 'notmuch' backend.
*rmdir* [-f]
Removes the current folder.
By default, it will fail if the directory is non-empty (see *-f*).
*-f*
Remove the directory even if it contains messages.
This is not supported on the 'notmuch' backend.
Some programs that sync maildirs may recover deleted directories (e.g.
offlineimap). These can either be specially configured to properly
handle directory deletion, or special commands need to be run to delete
directories (e.g. 'offlineimap --delete-folder').
It is possible, with a slow connection and the 'imap' backend, that new
messages arrive in the directory before they show up - using 'rmdir' at
this moment would delete the directory and such new messages before the
user sees them.
*next* <n>[%], *prev* <n>[%] *next* <n>[%], *prev* <n>[%]
Selects the next (or previous) message in the message list. If specified as Selects the next (or previous) message in the message list. If specified as
a percentage, the percentage is applied to the number of messages shown on a percentage, the percentage is applied to the number of messages shown on

View file

@ -237,6 +237,8 @@ func (acct *AccountView) onMessage(msg types.WorkerMessage) {
} }
case *types.CreateDirectory: case *types.CreateDirectory:
acct.dirlist.UpdateList(nil) acct.dirlist.UpdateList(nil)
case *types.RemoveDirectory:
acct.dirlist.UpdateList(nil)
} }
case *types.DirectoryInfo: case *types.DirectoryInfo:
if store, ok := acct.dirlist.MsgStore(msg.Info.Name); ok { if store, ok := acct.dirlist.MsgStore(msg.Info.Name); ok {

19
worker/imap/remove.go Normal file
View file

@ -0,0 +1,19 @@
package imap
import (
"git.sr.ht/~sircmpwn/aerc/worker/types"
)
func (imapw *IMAPWorker) handleRemoveDirectory(msg *types.RemoveDirectory) {
if err := imapw.client.Delete(msg.Directory); err != nil {
if msg.Quiet {
return
}
imapw.worker.PostMessage(&types.Error{
Message: types.RespondTo(msg),
Error: err,
}, nil)
} else {
imapw.worker.PostMessage(&types.Done{types.RespondTo(msg)}, nil)
}
}

View file

@ -165,6 +165,8 @@ func (w *IMAPWorker) handleMessage(msg types.WorkerMessage) error {
w.handleFetchDirectoryContents(msg) w.handleFetchDirectoryContents(msg)
case *types.CreateDirectory: case *types.CreateDirectory:
w.handleCreateDirectory(msg) w.handleCreateDirectory(msg)
case *types.RemoveDirectory:
w.handleRemoveDirectory(msg)
case *types.FetchMessageHeaders: case *types.FetchMessageHeaders:
w.handleFetchMessageHeaders(msg) w.handleFetchMessageHeaders(msg)
case *types.FetchMessageBodyPart: case *types.FetchMessageBodyPart:

View file

@ -186,6 +186,8 @@ func (w *Worker) handleMessage(msg types.WorkerMessage) error {
return w.handleFetchDirectoryContents(msg) return w.handleFetchDirectoryContents(msg)
case *types.CreateDirectory: case *types.CreateDirectory:
return w.handleCreateDirectory(msg) return w.handleCreateDirectory(msg)
case *types.RemoveDirectory:
return w.handleRemoveDirectory(msg)
case *types.FetchMessageHeaders: case *types.FetchMessageHeaders:
return w.handleFetchMessageHeaders(msg) return w.handleFetchMessageHeaders(msg)
case *types.FetchMessageBodyPart: case *types.FetchMessageBodyPart:
@ -362,6 +364,16 @@ func (w *Worker) handleCreateDirectory(msg *types.CreateDirectory) error {
return nil return nil
} }
func (w *Worker) handleRemoveDirectory(msg *types.RemoveDirectory) error {
dir := w.c.Dir(msg.Directory)
if err := os.RemoveAll(string(dir)); err != nil {
w.worker.Logger.Printf("could not remove directory %s: %v",
msg.Directory, err)
return err
}
return nil
}
func (w *Worker) handleFetchMessageHeaders( func (w *Worker) handleFetchMessageHeaders(
msg *types.FetchMessageHeaders) error { msg *types.FetchMessageHeaders) error {
for _, uid := range msg.Uids { for _, uid := range msg.Uids {

View file

@ -131,6 +131,8 @@ func (w *worker) handleMessage(msg types.WorkerMessage) error {
// return w.handleAppendMessage(msg) // return w.handleAppendMessage(msg)
// case *types.CreateDirectory: // case *types.CreateDirectory:
// return w.handleCreateDirectory(msg) // return w.handleCreateDirectory(msg)
// case *types.RemoveDirectory:
// return w.handleRemoveDirectory(msg)
} }
return errUnsupported return errUnsupported
} }

View file

@ -92,6 +92,12 @@ type CreateDirectory struct {
Quiet bool Quiet bool
} }
type RemoveDirectory struct {
Message
Directory string
Quiet bool
}
type FetchMessageHeaders struct { type FetchMessageHeaders struct {
Message Message
Uids []uint32 Uids []uint32