~sircmpwn/aerc

This thread contains a patchset. You're looking at the original emails, but you may wish to use the patch review UI. Review patch
1

[PATCH v3] Add archive command

Details
Message ID
<20190608174156.17289-1-r@gnzler.io>
Sender timestamp
1560015716
DKIM signature
missing
Download raw message
Patch: +168 -8
Adds an archive command that moves the current message into the folder
specified in the account config entry.

Supports three layouts at this point:

- flat: puts all messages next to each other
- year: creates a folder per year
- month: same as above, plus folders per month

This also adds a "-p" argument to "cp" and "mv" that works like
"--parents" on mkdir(1). We use this to auto-create the directories
for the archive layout.
---
ok I think I addressed pretty much all your feedback with this one.

I split the creation of the cp/mv destination out of the respective
msgstore methods again (I had it like this in v0 but decided to scrap it)
and moved it into it's own worker action. That enables us to handle the
directory creation from the account widget as you suggested.

I also added default bindings and documentation :)

 commands/msg/archive.go  | 62 ++++++++++++++++++++++++++++++++++++++++
 commands/msg/copy.go     | 23 +++++++++++++--
 commands/msg/move.go     | 23 +++++++++++++--
 config/binds.conf        |  2 ++
 config/config.go         |  4 +++
 doc/aerc-config.5.scd    |  5 ++++
 doc/aerc.1.scd           |  9 ++++++
 lib/msgstore.go          | 17 +++++++++--
 widgets/account.go       |  2 ++
 worker/imap/create.go    | 22 ++++++++++++++
 worker/imap/worker.go    |  2 ++
 worker/types/messages.go |  5 ++++
 12 files changed, 168 insertions(+), 8 deletions(-)
 create mode 100644 commands/msg/archive.go
 create mode 100644 worker/imap/create.go

diff --git a/commands/msg/archive.go b/commands/msg/archive.go
new file mode 100644
index 0000000..11752f7
--- /dev/null
+++ b/commands/msg/archive.go
@@ -0,0 +1,62 @@
package msg

import (
	"errors"
	"fmt"
	"path"
	"time"

	"github.com/gdamore/tcell"

	"git.sr.ht/~sircmpwn/aerc/widgets"
	"git.sr.ht/~sircmpwn/aerc/worker/types"
)

const (
	ARCHIVE_FLAT  = "flat"
	ARCHIVE_YEAR  = "year"
	ARCHIVE_MONTH = "month"
)

func init() {
	register("archive", Archive)
}

func Archive(aerc *widgets.Aerc, args []string) error {
	if len(args) != 2 {
		return errors.New("Usage: archive <flat|year|month>")
	}
	acct := aerc.SelectedAccount()
	if acct == nil {
		return errors.New("No account selected")
	}
	msg := acct.Messages().Selected()
	store := acct.Messages().Store()
	archiveDir := acct.AccountConfig().Archive
	acct.Messages().Next()

	switch args[1] {
	case ARCHIVE_MONTH:
		archiveDir = path.Join(archiveDir,
			fmt.Sprintf("%d", msg.Envelope.Date.Year()),
			fmt.Sprintf("%02d", msg.Envelope.Date.Month()))
	case ARCHIVE_YEAR:
		archiveDir = path.Join(archiveDir, fmt.Sprintf("%v",
			msg.Envelope.Date.Year()))
	case ARCHIVE_FLAT:
		// deliberately left blank
	}

	store.Move([]uint32{msg.Uid}, archiveDir, true, func(
		msg types.WorkerMessage) {

		switch msg := msg.(type) {
		case *types.Done:
			aerc.PushStatus("Messages archived.", 10*time.Second)
		case *types.Error:
			aerc.PushStatus(" "+msg.Error.Error(), 10*time.Second).
				Color(tcell.ColorDefault, tcell.ColorRed)
		}
	})
	return nil
}
diff --git a/commands/msg/copy.go b/commands/msg/copy.go
index 57c93a3..0d9836b 100644
--- a/commands/msg/copy.go
+++ b/commands/msg/copy.go
@@ -4,6 +4,7 @@ import (
	"errors"
	"time"

	"git.sr.ht/~sircmpwn/getopt"
	"github.com/gdamore/tcell"

	"git.sr.ht/~sircmpwn/aerc/widgets"
@@ -16,9 +17,23 @@ func init() {
}

func Copy(aerc *widgets.Aerc, args []string) error {
	if len(args) != 2 {
		return errors.New("Usage: mv <folder>")
	opts, optind, err := getopt.Getopts(args[1:], "p")
	if err != nil {
		return err
	}
	if optind != len(args)-2 {
		return errors.New("Usage: cp [-p] <folder>")
	}
	var (
		createParents bool
	)
	for _, opt := range opts {
		switch opt.Option {
		case 'p':
			createParents = true
		}
	}

	widget := aerc.SelectedTab().(widgets.ProvidesMessage)
	acct := widget.SelectedAccount()
	if acct == nil {
@@ -26,7 +41,9 @@ func Copy(aerc *widgets.Aerc, args []string) error {
	}
	msg := widget.SelectedMessage()
	store := widget.Store()
	store.Copy([]uint32{msg.Uid}, args[1], func(msg types.WorkerMessage) {
	store.Copy([]uint32{msg.Uid}, args[optind+1], createParents, func(
		msg types.WorkerMessage) {

		switch msg := msg.(type) {
		case *types.Done:
			aerc.PushStatus("Messages copied.", 10*time.Second)
diff --git a/commands/msg/move.go b/commands/msg/move.go
index 1224efa..7742ffb 100644
--- a/commands/msg/move.go
+++ b/commands/msg/move.go
@@ -4,6 +4,7 @@ import (
	"errors"
	"time"

	"git.sr.ht/~sircmpwn/getopt"
	"github.com/gdamore/tcell"

	"git.sr.ht/~sircmpwn/aerc/widgets"
@@ -16,9 +17,23 @@ func init() {
}

func Move(aerc *widgets.Aerc, args []string) error {
	if len(args) != 2 {
		return errors.New("Usage: mv <folder>")
	opts, optind, err := getopt.Getopts(args[1:], "p")
	if err != nil {
		return err
	}
	if optind != len(args)-2 {
		return errors.New("Usage: mv [-p] <folder>")
	}
	var (
		createParents bool
	)
	for _, opt := range opts {
		switch opt.Option {
		case 'p':
			createParents = true
		}
	}

	widget := aerc.SelectedTab().(widgets.ProvidesMessage)
	acct := widget.SelectedAccount()
	if acct == nil {
@@ -31,7 +46,9 @@ func Move(aerc *widgets.Aerc, args []string) error {
		aerc.RemoveTab(widget)
	}
	acct.Messages().Next()
	store.Move([]uint32{msg.Uid}, args[1], func(msg types.WorkerMessage) {
	store.Move([]uint32{msg.Uid}, args[optind+1], createParents, func(
		msg types.WorkerMessage) {

		switch msg := msg.(type) {
		case *types.Done:
			aerc.PushStatus("Messages moved.", 10*time.Second)
diff --git a/config/binds.conf b/config/binds.conf
index 40255de..1ef9542 100644
--- a/config/binds.conf
+++ b/config/binds.conf
@@ -28,6 +28,7 @@ K = :prev-folder<Enter>
<Enter> = :view<Enter>
d = :confirm 'Really delete this message?' ':delete-message<Enter>'<Enter>
D = :delete<Enter>
A = :archive flat<Enter>

C = :compose<Enter>

@@ -54,6 +55,7 @@ Rq = :reply -aq<Enter>
<C-k> = :prev-part<Enter>
<C-j> = :next-part<Enter>
S = :save<space>
A = :archive flat<Enter>

[compose]
# Keybindings used when the embedded terminal is not selected in the compose
diff --git a/config/config.go b/config/config.go
index 3ef587b..8e669f7 100644
--- a/config/config.go
+++ b/config/config.go
@@ -34,6 +34,7 @@ const (
)

type AccountConfig struct {
	Archive         string
	CopyTo          string
	Default         string
	From            string
@@ -115,6 +116,7 @@ func loadAccountConfig(path string) ([]AccountConfig, error) {
		}
		sec := file.Section(_sec)
		account := AccountConfig{
			Archive: "Archive",
			Default: "INBOX",
			Name:    _sec,
			Params:  make(map[string]string),
@@ -137,6 +139,8 @@ func loadAccountConfig(path string) ([]AccountConfig, error) {
				account.From = val
			} else if key == "copy-to" {
				account.CopyTo = val
			} else if key == "archive" {
				account.Archive = val
			} else if key != "name" {
				account.Params[key] = val
			}
diff --git a/doc/aerc-config.5.scd b/doc/aerc-config.5.scd
index e002764..caf971d 100644
--- a/doc/aerc-config.5.scd
+++ b/doc/aerc-config.5.scd
@@ -106,6 +106,11 @@ Note that many of these configuration options are written for you, such as
*source* and *outgoing*, when you run the account configuration wizard
(*:new-account*).

*archive*
	Specifies a folder to use as the destination of the *:archive* command.

	Default: Archive

*copy-to*
	Specifies a folder to copy sent mails to, usually "Sent".

diff --git a/doc/aerc.1.scd b/doc/aerc.1.scd
index ca835e5..eab0cb3 100644
--- a/doc/aerc.1.scd
+++ b/doc/aerc.1.scd
@@ -43,6 +43,15 @@ These commands work in any context.

## MESSAGE LIST COMMANDS

*archive* <scheme>
	Moves the selected message to the archive. The available schemes are:

	*flat*: No special structure, all messages in the archive directory

	*year*: Messages are stored in folders per year

	*month*: Messages are stored in folders per year and subfolders per month

*cf* <folder>
	Change the folder shown in the message list.

diff --git a/lib/msgstore.go b/lib/msgstore.go
index 6ab7fc2..900ec16 100644
--- a/lib/msgstore.go
+++ b/lib/msgstore.go
@@ -218,20 +218,27 @@ func (store *MessageStore) Delete(uids []uint32,
	store.update()
}

func (store *MessageStore) Copy(uids []uint32, dest string,
func (store *MessageStore) Copy(uids []uint32, dest string, createDest bool,
	cb func(msg types.WorkerMessage)) {

	var set imap.SeqSet
	for _, uid := range uids {
		set.AddNum(uid)
	}

	if createDest {
		store.worker.PostAction(&types.CreateDirectory{
			Directory: dest,
		}, cb)
	}

	store.worker.PostAction(&types.CopyMessages{
		Destination: dest,
		Uids:        set,
	}, cb)
}

func (store *MessageStore) Move(uids []uint32, dest string,
func (store *MessageStore) Move(uids []uint32, dest string, createDest bool,
	cb func(msg types.WorkerMessage)) {

	var set imap.SeqSet
@@ -240,6 +247,12 @@ func (store *MessageStore) Move(uids []uint32, dest string,
		store.Deleted[uid] = nil
	}

	if createDest {
		store.worker.PostAction(&types.CreateDirectory{
			Directory: dest,
		}, cb)
	}

	store.worker.PostAction(&types.CopyMessages{
		Destination: dest,
		Uids:        set,
diff --git a/widgets/account.go b/widgets/account.go
index 1921dbd..72874b0 100644
--- a/widgets/account.go
+++ b/widgets/account.go
@@ -185,6 +185,8 @@ func (acct *AccountView) onMessage(msg types.WorkerMessage) {
			} else {
				acct.msglist.SetStore(nil)
			}
		case *types.CreateDirectory:
			acct.dirlist.UpdateList(nil)
		}
	case *types.DirectoryInfo:
		if store, ok := acct.msgStores[msg.Name]; ok {
diff --git a/worker/imap/create.go b/worker/imap/create.go
new file mode 100644
index 0000000..3cc71c5
--- /dev/null
+++ b/worker/imap/create.go
@@ -0,0 +1,22 @@
package imap

import (
	"strings"

	"git.sr.ht/~sircmpwn/aerc/worker/types"
)

func (imapw *IMAPWorker) handleCreateDirectory(msg *types.CreateDirectory) {
	if err := imapw.client.Create(msg.Directory); err != nil {
		if strings.HasPrefix(err.Error(), "Mailbox already exists") {
			// ignore "already exists" error
			return
		}
		imapw.worker.PostMessage(&types.Error{
			Message: types.RespondTo(msg),
			Error:   err,
		}, nil)
	} else {
		imapw.worker.PostMessage(&types.Done{types.RespondTo(msg)}, nil)
	}
}
diff --git a/worker/imap/worker.go b/worker/imap/worker.go
index 125fba8..f71a950 100644
--- a/worker/imap/worker.go
+++ b/worker/imap/worker.go
@@ -127,6 +127,8 @@ func (w *IMAPWorker) handleMessage(msg types.WorkerMessage) error {
		w.handleOpenDirectory(msg)
	case *types.FetchDirectoryContents:
		w.handleFetchDirectoryContents(msg)
	case *types.CreateDirectory:
		w.handleCreateDirectory(msg)
	case *types.FetchMessageHeaders:
		w.handleFetchMessageHeaders(msg)
	case *types.FetchMessageBodyPart:
diff --git a/worker/types/messages.go b/worker/types/messages.go
index 29d3d9f..0d81c4f 100644
--- a/worker/types/messages.go
+++ b/worker/types/messages.go
@@ -82,6 +82,11 @@ type FetchDirectoryContents struct {
	Message
}

type CreateDirectory struct {
	Message
	Directory string
}

type FetchMessageHeaders struct {
	Message
	Uids imap.SeqSet
-- 
2.21.0
Details
Message ID
<BUPBCLDXFBQP.16DDFWUTXGJTE@homura>
In-Reply-To
<20190608174156.17289-1-r@gnzler.io> (view parent)
Sender timestamp
1560094513
DKIM signature
missing
Download raw message
Looks good to me, thanks!

To git.sr.ht:~sircmpwn/aerc
   35f5732..acfe7d7  master -> master

A nice follow-up for this would be implementing the mkdir command, if
you're up for it.
Reply to thread Export thread (mbox)