~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 v2] Add archive command

Details
Message ID
<20190606095050.29563-1-r@gnzler.io>
Sender timestamp
1559814650
DKIM signature
missing
Download raw message
Patch: +126 -16
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.
---
 commands/msg/archive.go  | 59 ++++++++++++++++++++++++++++++++++++++++
 commands/msg/copy.go     | 24 ++++++++++++++--
 commands/msg/move.go     | 24 ++++++++++++++--
 config/binds.conf        |  1 +
 config/config.go         |  3 ++
 lib/msgstore.go          | 16 +++++------
 worker/imap/movecopy.go  | 10 +++++++
 worker/types/messages.go |  5 ++--
 8 files changed, 126 insertions(+), 16 deletions(-)
 create mode 100644 commands/msg/archive.go

diff --git a/commands/msg/archive.go b/commands/msg/archive.go
new file mode 100644
index 0000000..ce4a6c3
--- /dev/null
+++ b/commands/msg/archive.go
@@ -0,0 +1,59 @@
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:
	}

	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)
			acct.Directories().UpdateList(nil)
		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..a03a1d9 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,10 +41,13 @@ 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)
			if createParents {
				acct.Directories().UpdateList(nil)
			}
		case *types.Error:
			aerc.PushStatus(" "+msg.Error.Error(), 10*time.Second).
				Color(tcell.ColorDefault, tcell.ColorRed)
diff --git a/commands/msg/move.go b/commands/msg/move.go
index 1224efa..78a60ba 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,10 +46,13 @@ 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)
			if createParents {
				acct.Directories().UpdateList(nil)
			}
		case *types.Error:
			aerc.PushStatus(" "+msg.Error.Error(), 10*time.Second).
				Color(tcell.ColorDefault, tcell.ColorRed)
diff --git a/config/binds.conf b/config/binds.conf
index f758b17..2b27454 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 year<Enter>

C = :compose<Enter>

diff --git a/config/config.go b/config/config.go
index 1fb764b..1c419c2 100644
--- a/config/config.go
+++ b/config/config.go
@@ -33,6 +33,7 @@ const (
)

type AccountConfig struct {
	Archive         string
	CopyTo          string
	Default         string
	From            string
@@ -133,6 +134,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/lib/msgstore.go b/lib/msgstore.go
index 6ab7fc2..dac6875 100644
--- a/lib/msgstore.go
+++ b/lib/msgstore.go
@@ -218,7 +218,7 @@ func (store *MessageStore) Delete(uids []uint32,
	store.update()
}

func (store *MessageStore) Copy(uids []uint32, dest string,
func (store *MessageStore) Copy(uids []uint32, dest string, createParents bool,
	cb func(msg types.WorkerMessage)) {
	var set imap.SeqSet
	for _, uid := range uids {
@@ -226,23 +226,23 @@ func (store *MessageStore) Copy(uids []uint32, dest string,
	}

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

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

	var set imap.SeqSet
	for _, uid := range uids {
		set.AddNum(uid)
		store.Deleted[uid] = nil
	}

	store.worker.PostAction(&types.CopyMessages{
		Destination: dest,
		Uids:        set,
		Destination:       dest,
		Uids:              set,
		CreateDestination: createParents,
	}, func(msg types.WorkerMessage) {
		switch msg.(type) {
		case *types.Error:
diff --git a/worker/imap/movecopy.go b/worker/imap/movecopy.go
index 6cf3fe1..db5d300 100644
--- a/worker/imap/movecopy.go
+++ b/worker/imap/movecopy.go
@@ -2,11 +2,21 @@ package imap

import (
	"io"
	"strings"

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

func (imapw *IMAPWorker) handleCopyMessages(msg *types.CopyMessages) {
	if msg.CreateDestination {
		err := imapw.client.Create(msg.Destination)
		if err != nil && !strings.HasPrefix(err.Error(), "Mailbox already exists") {
			imapw.worker.PostMessage(&types.Error{
				Message: types.RespondTo(msg),
				Error:   err,
			}, nil)
		}
	}
	if err := imapw.client.UidCopy(&msg.Uids, msg.Destination); err != nil {
		imapw.worker.PostMessage(&types.Error{
			Message: types.RespondTo(msg),
diff --git a/worker/types/messages.go b/worker/types/messages.go
index 4e46cbf..5a00bbf 100644
--- a/worker/types/messages.go
+++ b/worker/types/messages.go
@@ -104,8 +104,9 @@ type DeleteMessages struct {

type CopyMessages struct {
	Message
	Destination string
	Uids        imap.SeqSet
	Destination       string
	Uids              imap.SeqSet
	CreateDestination bool
}

type AppendMessage struct {
-- 
2.21.0
Details
Message ID
<BUNK7N51XEOH.6SMHCWUUQLY8@homura>
In-Reply-To
<20190606095050.29563-1-r@gnzler.io> (view parent)
Sender timestamp
1559916395
DKIM signature
missing
Download raw message
Thanks for the patch! I have just a few more comments.

On Thu Jun 6, 2019 at 11:50 AM Robert Günzler wrote:
> +	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:
> +	}

Could use a /* this space deliberately left blank */ here on the last
case branch

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

80 col limit

> +		switch msg := msg.(type) {
> +		case *types.Done:
> +			aerc.PushStatus("Messages archived.", 10*time.Second)
> +			acct.Directories().UpdateList(nil)

Perhaps this UpdateList(nil) might be more suitable for the store to do
somehow? Like bubble up a callback from the store when it creates new
directories, then have the account widget update the list, idk

> diff --git a/commands/msg/move.go b/commands/msg/move.go
> index 1224efa..78a60ba 100644
> --- a/commands/msg/move.go
> +++ b/commands/msg/move.go
> -%<-
> -	store.Move([]uint32{msg.Uid}, args[1], func(msg types.WorkerMessage) {
> +	store.Move([]uint32{msg.Uid}, args[optind+1], createParents, func(msg types.WorkerMessage) {

80 col limit

>  type AccountConfig struct {
> +	Archive         string

Let's go ahead and default this to "Archive" even if the user doesn't
specify it in their config. Speaking of config things, feel free to add
a default keybinding to the msglist & viewer contexts:

A = :archive flat<Enter>
Reply to thread Export thread (mbox)