~sircmpwn/aerc

Add flag/unflag command v1 PROPOSED

Srivathsan Murali: 1
 Add flag/unflag command

 10 files changed, 323 insertions(+), 0 deletions(-)
Export patchset (mbox)
How do I use this?

Copy & paste the following snippet into your terminal to import this patchset into git:

curl -s https://lists.sr.ht/~sircmpwn/aerc/patches/11356/mbox | git am -3
Learn more about email & git
View this thread in the archives

[PATCH] Add flag/unflag command Export this patch

---
Was trying to flag an important email for later, I realized that a
command was not implemented for this. So I figured I would send in a
patch.
I have tested it on maildir. Not tested on imap or notmuch.
 commands/msg/flag.go      | 147 ++++++++++++++++++++++++++++++++++++++
 doc/aerc.1.scd            |  10 +++
 lib/msgstore.go           |   9 +++
 worker/imap/flags.go      |  32 +++++++++
 worker/imap/worker.go     |   2 +
 worker/maildir/message.go |  20 ++++++
 worker/maildir/worker.go  |  34 +++++++++
 worker/notmuch/message.go |  33 +++++++++
 worker/notmuch/worker.go  |  29 ++++++++
 worker/types/messages.go  |   7 ++
 10 files changed, 323 insertions(+)
 create mode 100644 commands/msg/flag.go

diff --git a/commands/msg/flag.go b/commands/msg/flag.go
new file mode 100644
index 000000000000..76e54c675259
--- /dev/null
+++ b/commands/msg/flag.go
@@ -0,0 +1,147 @@
package msg

import (
	"errors"
	"sync"
	"time"

	"git.sr.ht/~sircmpwn/getopt"

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

type Flag struct{}

func init() {
	register(Flag{})
}

func (Flag) Aliases() []string {
	return []string{"flag", "unflag"}
}

func (Flag) Complete(aerc *widgets.Aerc, args []string) []string {
	return nil
}

func (Flag) Execute(aerc *widgets.Aerc, args []string) error {
	opts, optind, err := getopt.Getopts(args, "t")
	if err != nil {
		return err
	}
	if optind != len(args) {
		return errors.New("Usage: " + args[0] + " [-t]")
	}
	var toggle bool

	for _, opt := range opts {
		switch opt.Option {
		case 't':
			toggle = true
		}
	}

	h := newHelper(aerc)
	store, err := h.store()
	if err != nil {
		return err
	}

	if toggle {
		// ignore command given, simply toggle all the read states
		return submitFlagToggle(aerc, store, h)
	}
	msgUids, err := h.markedOrSelectedUids()
	if err != nil {
		return err
	}
	switch args[0] {
	case "flag":
		submitFlagChange(aerc, store, msgUids, true)
	case "unflag":
		submitFlagChange(aerc, store, msgUids, false)

	}
	return nil
}

func splitFlaggedMessages(msgs []*models.MessageInfo) (flaggedMsgs []uint32,
	unflaggedMsgs []uint32) {
	for _, m := range msgs {
		var flagged bool
		for _, flag := range m.Flags {
			if flag == models.FlaggedFlag {
				flagged = true
				break
			}
		}
		if flagged {
			flaggedMsgs = append(flaggedMsgs, m.Uid)
		} else {
			unflaggedMsgs = append(unflaggedMsgs, m.Uid)
		}
	}
	return flaggedMsgs, unflaggedMsgs
}

func submitFlagChange(aerc *widgets.Aerc, store *lib.MessageStore,
	uids []uint32, newState bool) {
	store.Flag(uids, newState, func(msg types.WorkerMessage) {
		switch msg := msg.(type) {
		case *types.Done:
			aerc.PushStatus(flag_msg_success, 10*time.Second)
		case *types.Error:
			aerc.PushError(" " + msg.Error.Error())
		}
	})
}

func submitFlagChangeWg(aerc *widgets.Aerc, store *lib.MessageStore,
	uids []uint32, newState bool, wg *sync.WaitGroup, success *bool) {
	store.Flag(uids, newState, func(msg types.WorkerMessage) {
		wg.Add(1)
		switch msg := msg.(type) {
		case *types.Done:
			wg.Done()
		case *types.Error:
			aerc.PushError(" " + msg.Error.Error())
			*success = false
			wg.Done()
		}
	})
}

func submitFlagToggle(aerc *widgets.Aerc, store *lib.MessageStore, h *helper) error {
	msgs, err := h.messages()
	if err != nil {
		return err
	}
	flagged, unflagged := splitFlaggedMessages(msgs)

	var wg sync.WaitGroup
	success := true

	if len(flagged) != 0 {
		newState := false
		submitFlagChangeWg(aerc, store, flagged, newState, &wg, &success)
	}

	if len(unflagged) != 0 {
		newState := true
		submitFlagChangeWg(aerc, store, unflagged, newState, &wg, &success)
	}
	// we need to do that in the background, else we block the main thread
	go func() {
		wg.Wait()
		if success {
			aerc.PushStatus(flag_msg_success, 10*time.Second)
		}
	}()
	return nil

}

const flag_msg_success = "flagged state set successfully"
diff --git a/doc/aerc.1.scd b/doc/aerc.1.scd
index 61e35bbca672..43da457dc6e4 100644
--- a/doc/aerc.1.scd
+++ b/doc/aerc.1.scd
@@ -146,6 +146,16 @@ message list, the message in the message viewer, etc).

	*-t*: Toggle the messages between read and unread.

*flag*
	Marks the marked or selected messages as flagged.

	*-t*: Toggle the messages between flagged and unflagged.

*unflag*
	Marks the marked or selected messages as unflagged.

	*-t*: Toggle the messages between flagged and unflagged.

*modify-labels* <[+-]label>...
	Modify message labels (e.g. notmuch tags). Labels prefixed with a '+' are
	added, those prefixed with a '-' removed. As a convenience, labels without
diff --git a/lib/msgstore.go b/lib/msgstore.go
index 86215a720be0..7dcb7c8d3fe2 100644
--- a/lib/msgstore.go
+++ b/lib/msgstore.go
@@ -342,6 +342,15 @@ func (store *MessageStore) Read(uids []uint32, read bool,
	}, cb)
}

func (store *MessageStore) Flag(uids []uint32, flagged bool,
	cb func(msg types.WorkerMessage)) {

	store.worker.PostAction(&types.FlagMessages{
		Flagged: flagged,
		Uids:    uids,
	}, cb)
}

func (store *MessageStore) Answered(uids []uint32, answered bool,
	cb func(msg types.WorkerMessage)) {

diff --git a/worker/imap/flags.go b/worker/imap/flags.go
index 262508669b93..b79263905106 100644
--- a/worker/imap/flags.go
+++ b/worker/imap/flags.go
@@ -76,6 +76,38 @@ func (imapw *IMAPWorker) handleAnsweredMessages(msg *types.AnsweredMessages) {
	})
}

func (imapw *IMAPWorker) handleFlagMessages(msg *types.FlagMessages) {
	item := imap.FormatFlagsOp(imap.AddFlags, true)
	flags := []interface{}{imap.FlaggedFlag}
	if !msg.Flagged {
		item = imap.FormatFlagsOp(imap.RemoveFlags, true)
		flags = []interface{}{imap.FlaggedFlag}
	}
	uids := toSeqSet(msg.Uids)
	emitErr := func(err error) {
		imapw.worker.PostMessage(&types.Error{
			Message: types.RespondTo(msg),
			Error:   err,
		}, nil)
	}
	if err := imapw.client.UidStore(uids, item, flags, nil); err != nil {
		emitErr(err)
		return
	}
	imapw.worker.PostAction(&types.FetchMessageHeaders{
		Uids: msg.Uids,
	}, func(_msg types.WorkerMessage) {
		switch m := _msg.(type) {
		case *types.Error:
			err := fmt.Errorf("handleFlagMessages: %v", m.Error)
			imapw.worker.Logger.Printf("could not fetch headers: %s", err)
			emitErr(err)
		case *types.Done:
			imapw.worker.PostMessage(&types.Done{types.RespondTo(msg)}, nil)
		}
	})
}

func (imapw *IMAPWorker) handleReadMessages(msg *types.ReadMessages) {
	item := imap.FormatFlagsOp(imap.AddFlags, true)
	flags := []interface{}{imap.SeenFlag}
diff --git a/worker/imap/worker.go b/worker/imap/worker.go
index a43ac49efdac..89a2f7961c14 100644
--- a/worker/imap/worker.go
+++ b/worker/imap/worker.go
@@ -175,6 +175,8 @@ func (w *IMAPWorker) handleMessage(msg types.WorkerMessage) error {
		w.handleDeleteMessages(msg)
	case *types.ReadMessages:
		w.handleReadMessages(msg)
	case *types.FlagMessages:
		w.handleFlagMessages(msg)
	case *types.AnsweredMessages:
		w.handleAnsweredMessages(msg)
	case *types.CopyMessages:
diff --git a/worker/maildir/message.go b/worker/maildir/message.go
index 5c6c9307d945..5f191cc3e0b4 100644
--- a/worker/maildir/message.go
+++ b/worker/maildir/message.go
@@ -92,6 +92,26 @@ func (m Message) MarkRead(seen bool) error {
	return m.SetFlags(newFlags)
}

// MarkFlagged either adds or removes the maildir.FlagFlagged flag from the
// message.
func (m Message) MarkFlagged(flagged bool) error {
	flags, err := m.Flags()
	if err != nil {
		return fmt.Errorf("could not read previous flags: %v", err)
	}
	if flagged {
		flags = append(flags, maildir.FlagFlagged)
		return m.SetFlags(flags)
	}
	var newFlags []maildir.Flag
	for _, flag := range flags {
		if flag != maildir.FlagFlagged {
			newFlags = append(newFlags, flag)
		}
	}
	return m.SetFlags(newFlags)
}

// Remove deletes the email immediately.
func (m Message) Remove() error {
	return m.dir.Remove(m.key)
diff --git a/worker/maildir/worker.go b/worker/maildir/worker.go
index f14672e5a819..29064af07148 100644
--- a/worker/maildir/worker.go
+++ b/worker/maildir/worker.go
@@ -195,6 +195,8 @@ func (w *Worker) handleMessage(msg types.WorkerMessage) error {
		return w.handleDeleteMessages(msg)
	case *types.ReadMessages:
		return w.handleReadMessages(msg)
	case *types.FlagMessages:
		return w.handleFlagMessages(msg)
	case *types.AnsweredMessages:
		return w.handleAnsweredMessages(msg)
	case *types.CopyMessages:
@@ -473,6 +475,38 @@ func (w *Worker) handleAnsweredMessages(msg *types.AnsweredMessages) error {
	return nil
}

func (w *Worker) handleFlagMessages(msg *types.FlagMessages) error {
	for _, uid := range msg.Uids {
		m, err := w.c.Message(*w.selected, uid)
		if err != nil {
			w.worker.Logger.Printf("could not get message: %v", err)
			w.err(msg, err)
			continue
		}
		if err := m.MarkFlagged(msg.Flagged); err != nil {
			w.worker.Logger.Printf("could not mark message as flagged: %v", err)
			w.err(msg, err)
			continue
		}
		info, err := m.MessageInfo()
		if err != nil {
			w.worker.Logger.Printf("could not get message info: %v", err)
			w.err(msg, err)
			continue
		}

		w.worker.PostMessage(&types.MessageInfo{
			Message: types.RespondTo(msg),
			Info:    info,
		}, nil)

		w.worker.PostMessage(&types.DirectoryInfo{
			Info: w.getDirectoryInfo(w.selectedName),
		}, nil)
	}
	return nil
}

func (w *Worker) handleReadMessages(msg *types.ReadMessages) error {
	for _, uid := range msg.Uids {
		m, err := w.c.Message(*w.selected, uid)
diff --git a/worker/notmuch/message.go b/worker/notmuch/message.go
index 65295073b839..8ccc10859760 100644
--- a/worker/notmuch/message.go
+++ b/worker/notmuch/message.go
@@ -97,6 +97,39 @@ func (m *Message) MarkAnswered(answered bool) error {
	return nil
}

// MarkFlagged either adds or removes the "flagged" tag from the message.
func (m *Message) MarkFlagged(flagged bool) error {
	haveFlagged := false
	tags, err := m.Tags()
	if err != nil {
		return err
	}
	for _, t := range tags {
		if t == "flagged" {
			haveFlagged = true
			break
		}
	}
	if haveFlagged == flagged {
		// we already have the desired state
		return nil
	}

	if haveFlagged {
		err := m.RemoveTag("flagged")
		if err != nil {
			return err
		}
		return nil
	}

	err = m.AddTag("flagged")
	if err != nil {
		return err
	}
	return nil
}

// MarkRead either adds or removes the maildir.FlagSeen flag from the message.
func (m *Message) MarkRead(seen bool) error {
	haveUnread := false
diff --git a/worker/notmuch/worker.go b/worker/notmuch/worker.go
index b22626981068..a30fe1d17e0e 100644
--- a/worker/notmuch/worker.go
+++ b/worker/notmuch/worker.go
@@ -115,6 +115,8 @@ func (w *worker) handleMessage(msg types.WorkerMessage) error {
		return w.handleFetchFullMessages(msg)
	case *types.ReadMessages:
		return w.handleReadMessages(msg)
	case *types.FlagMessages:
		return w.handleFlagMessages(msg)
	case *types.SearchDirectory:
		return w.handleSearchDirectory(msg)
	case *types.ModifyLabels:
@@ -390,6 +392,33 @@ func (w *worker) handleAnsweredMessages(msg *types.AnsweredMessages) error {
	return nil
}

func (w *worker) handleFlagMessages(msg *types.FlagMessages) error {
	for _, uid := range msg.Uids {
		m, err := w.msgFromUid(uid)
		if err != nil {
			w.w.Logger.Printf("could not get message: %v", err)
			w.err(msg, err)
			continue
		}
		if err := m.MarkFlagged(msg.Flagged); err != nil {
			w.w.Logger.Printf("could not mark message as flagged: %v", err)
			w.err(msg, err)
			continue
		}
		err = w.emitMessageInfo(m, msg)
		if err != nil {
			w.w.Logger.Printf(err.Error())
			w.err(msg, err)
			continue
		}
	}
	if err := w.emitDirectoryInfo(w.currentQueryName); err != nil {
		w.w.Logger.Printf(err.Error())
	}
	w.done(msg)
	return nil
}

func (w *worker) handleReadMessages(msg *types.ReadMessages) error {
	for _, uid := range msg.Uids {
		m, err := w.msgFromUid(uid)
diff --git a/worker/types/messages.go b/worker/types/messages.go
index 475a7aa32bd8..944a130bc914 100644
--- a/worker/types/messages.go
+++ b/worker/types/messages.go
@@ -120,6 +120,13 @@ type ReadMessages struct {
	Uids []uint32
}

// Marks messages as flagged or unflagged
type FlagMessages struct {
	Message
	Flagged bool
	Uids    []uint32
}

type AnsweredMessages struct {
	Message
	Answered bool
-- 
2.27.0
Hi Srivathsan,

On Sat, Jun 27, 2020 at 11:40:46PM +0200, Srivathsan Murali wrote: