~rjarry/aerc-devel

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

[PATCH aerc v4 1/3] commands/mark: add filter-based marking

Moritz Poldrack <git@moritz.sh>
Details
Message ID
<20250218153834.18779-1-git@moritz.sh>
Sender timestamp
1739896660
DKIM signature
pass
Download raw message
Patch: +140 -15
This commit expands the :mark command with the ability to combine it
with filters to automatically mark all messages matching a filter. The
filters can be combined with -t and -T to narrow down the selected
messages:

:mark Delivered # mark all messages with "Delivered" in the Subject
:mark -s amazon # mark all messages from "amazon"
:unmark invoice # unmark all messages with "invoice" in the subject

Signed-off-by: Moritz Poldrack <git@moritz.sh>
---

Fixed a bug where -a would not mark all messages anymore (Thanks Matej!)

I'll just leave this in limbo until we inevitably upgrade the Go version
anyway ^^

 commands/msg/mark.go | 134 +++++++++++++++++++++++++++++++++++++++----
 doc/aerc.1.scd       |  19 ++++--
 go.mod               |   2 +-
 3 files changed, 140 insertions(+), 15 deletions(-)

diff --git a/commands/msg/mark.go b/commands/msg/mark.go
index 21ba2fc1..b71357da 100644
--- a/commands/msg/mark.go
+++ b/commands/msg/mark.go
@@ -2,17 +2,26 @@ package msg

import (
	"fmt"
	"iter"
	"slices"
	"strings"

	"git.sr.ht/~rjarry/aerc/commands"
	"git.sr.ht/~rjarry/aerc/lib"
	"git.sr.ht/~rjarry/aerc/lib/log"
	"git.sr.ht/~rjarry/aerc/models"
	"git.sr.ht/~rjarry/aerc/worker/types"
)

type Mark struct {
	All         bool `opt:"-a" aliases:"mark,unmark" desc:"Mark all messages in current folder."`
	Toggle      bool `opt:"-t" aliases:"mark,unmark" desc:"Toggle the marked state."`
	Visual      bool `opt:"-v" aliases:"mark,unmark" desc:"Enter / leave visual mark mode."`
	VisualClear bool `opt:"-V" aliases:"mark,unmark" desc:"Same as -v but does not clear existing selection."`
	Thread      bool `opt:"-T" aliases:"mark,unmark" desc:"Mark all messages from the selected thread."`
	All             bool   `opt:"-a" aliases:"mark,unmark" desc:"Mark all messages in current folder."`
	Toggle          bool   `opt:"-t" aliases:"mark,unmark" desc:"Toggle the marked state."`
	Visual          bool   `opt:"-v" aliases:"mark,unmark" desc:"Enter / leave visual mark mode."`
	VisualClear     bool   `opt:"-V" aliases:"mark,unmark" desc:"Same as -v but does not clear existing selection."`
	Thread          bool   `opt:"-T" aliases:"mark,unmark" desc:"Mark all messages from the selected thread."`
	SenderFilter    bool   `opt:"-s" aliases:"mark,unmark" desc:"Mark all messages sent from a sender containing the given substring."`
	RecipientFilter bool   `opt:"-r" aliases:"mark,unmark" desc:"Mark all messages received from an address containing the given substring."`
	FilterString    string `opt:"..." required:"false" desc:"Mark messages matching this string."`
}

func init() {
@@ -53,13 +62,34 @@ func (m Mark) Execute(args []string) error {
	if m.Thread && m.All {
		return fmt.Errorf("-a and -T are mutually exclusive")
	}

	if m.Thread && (m.Visual || m.VisualClear) {
		return fmt.Errorf("-v and -T are mutually exclusive")
	}
	if m.Visual && m.All {
		return fmt.Errorf("-a and -v are mutually exclusive")
	}
	if m.SenderFilter && m.RecipientFilter {
		return fmt.Errorf("-s and -r are mutually exclusive")
	}
	if m.Visual && m.FilterString != "" {
		return fmt.Errorf("visual mode does not support filtering")
	}
	if (m.SenderFilter || m.RecipientFilter) && m.FilterString == "" {
		return fmt.Errorf("-s and -r requre a filter string")
	}

	// if filtering and only a single message is provided, filter all
	// instead
	m.All = (m.FilterString != "" && !(m.Thread || m.All)) || m.All

	filter := slices.Values[[]models.UID, models.UID]
	if m.SenderFilter {
		filter = senderFilter(store, m.FilterString)
	} else if m.RecipientFilter {
		filter = recipientFilter(store, m.FilterString)
	} else if m.FilterString != "" {
		filter = subjectFilter(store, m.FilterString)
	}

	switch args[0] {
	case "mark":
@@ -72,7 +102,7 @@ func (m Mark) Execute(args []string) error {
		switch {
		case m.All:
			uids := store.Uids()
			for _, uid := range uids {
			for uid := range filter(uids) {
				modFunc(uid)
			}
			return nil
@@ -85,7 +115,7 @@ func (m Mark) Execute(args []string) error {
				if err != nil {
					return err
				}
				for _, uid := range threadPtr.Root().Uids() {
				for uid := range filter(threadPtr.Root().Uids()) {
					modFunc(uid)
				}
			} else {
@@ -102,7 +132,7 @@ func (m Mark) Execute(args []string) error {
		switch {
		case m.All && m.Toggle:
			uids := store.Uids()
			for _, uid := range uids {
			for uid := range filter(uids) {
				marker.ToggleMark(uid)
			}
			return nil
@@ -115,7 +145,7 @@ func (m Mark) Execute(args []string) error {
				if err != nil {
					return err
				}
				for _, uid := range threadPtr.Root().Uids() {
				for uid := range filter(threadPtr.Root().Uids()) {
					marker.Unmark(uid)
				}
			} else {
@@ -129,3 +159,87 @@ func (m Mark) Execute(args []string) error {
	}
	return nil // never reached
}

func senderFilter(store *lib.MessageStore, senderMatches string) func([]models.UID) iter.Seq[models.UID] {
	return func(uids []models.UID) iter.Seq[models.UID] {
		store.FetchHeaders(uids, func(types.WorkerMessage) {})

		store.Lock()
		defer store.Unlock()

		var filteredUIDs []models.UID
		for _, uid := range uids {
			log.Debugf("checking for %s in messageStore", uid)
			msg := store.Messages[uid]
			if msg == nil {
				log.Warnf("message not found in messageStore")
				continue
			}
			log.Debugf("message: %#v", msg)
			from := msg.Envelope.From
			for _, sender := range from {
				if strings.Contains(sender.String(), senderMatches) {
					filteredUIDs = append(filteredUIDs, uid)
					break
				}
			}
		}

		return slices.Values(filteredUIDs)
	}
}

func recipientFilter(store *lib.MessageStore, recipientMatches string) func([]models.UID) iter.Seq[models.UID] {
	return func(uids []models.UID) iter.Seq[models.UID] {
		store.FetchHeaders(uids, func(types.WorkerMessage) {})

		store.Lock()
		defer store.Unlock()

		var filteredUIDs []models.UID
		for _, uid := range uids {
			log.Debugf("checking for %s in messageStore", uid)
			msg := store.Messages[uid]
			if msg == nil {
				log.Warnf("message not found in messageStore")
				continue
			}
			log.Debugf("message: %#v", msg)
			recipients := msg.Envelope.To
			for _, recipient := range recipients {
				if strings.Contains(recipient.String(), recipientMatches) {
					filteredUIDs = append(filteredUIDs, uid)
					break
				}
			}
		}

		return slices.Values(filteredUIDs)
	}
}

func subjectFilter(store *lib.MessageStore, subjectMatches string) func([]models.UID) iter.Seq[models.UID] {
	return func(uids []models.UID) iter.Seq[models.UID] {
		store.FetchHeaders(uids, func(types.WorkerMessage) {})

		store.Lock()
		defer store.Unlock()

		var filteredUIDs []models.UID
		for _, uid := range uids {
			log.Debugf("checking for %s in messageStore", uid)
			msg := store.Messages[uid]
			if msg == nil {
				log.Warnf("message not found in messageStore")
				continue
			}
			log.Debugf("message: %#v", msg)
			subject := msg.Envelope.Subject
			if strings.Contains(subject, subjectMatches) {
				filteredUIDs = append(filteredUIDs, uid)
			}
		}

		return slices.Values(filteredUIDs)
	}
}
diff --git a/doc/aerc.1.scd b/doc/aerc.1.scd
index 70ea232a..233a1139 100644
--- a/doc/aerc.1.scd
+++ b/doc/aerc.1.scd
@@ -841,9 +841,11 @@ message list, the message in the message viewer, etc).

	*-A*: Same as *-a* but saves all the named parts, not just attachments.

*:mark* [*-atvT*]
*:mark* [*-atvTsr*] _<filter>_
	Marks messages. Commands will execute on all marked messages instead of the
	highlighted one if applicable. The flags below can be combined as needed.
	highlighted one if applicable. The flags below can be combined as
	needed. The existence of a filter implies *-a* unless *-T* has been
	specified.

	*-a*: Apply to all messages in the current folder

@@ -855,13 +857,22 @@ message list, the message in the message viewer, etc).

	*-T*: Marks the displayed message thread of the selected message.

*:unmark* [*-at*]
	Unmarks messages. The flags below can be combined as needed.
	*-s*: apply the filter to the From: header (does not work with *-v* or *-V*)

	*-r*: apply the filter to the To: header (does not work with *-v* or *-V*)

*:unmark* [*-atsr*] _<filter>_
	Unmarks messages. The flags below can be combined as needed. The
	existence of a filter implies *-a*.

	*-a*: Apply to all messages in the current folder

	*-t*: toggle the mark state instead of unmarking a message

	*-s*: apply the filter to the From: header (does not work with *-v* or *-V*)

	*-r*: apply the filter to the To: header (does not work with *-v* or *-V*)

*:remark*
	Re-select the last set of marked messages. Can be used to chain commands
	after a selection has been acted upon
diff --git a/go.mod b/go.mod
index 445d9712..52a7f63e 100644
--- a/go.mod
+++ b/go.mod
@@ -1,6 +1,6 @@
module git.sr.ht/~rjarry/aerc

go 1.21
go 1.23

require (
	git.sr.ht/~rjarry/go-opt/v2 v2.0.1
-- 
2.48.1

[PATCH aerc v4 2/3] docs: add missing -T to :unmark

Moritz Poldrack <git@moritz.sh>
Details
Message ID
<20250218153834.18779-2-git@moritz.sh>
In-Reply-To
<20250218153834.18779-1-git@moritz.sh> (view parent)
Sender timestamp
1739896661
DKIM signature
pass
Download raw message
Patch: +4 -2
The -T flag has been omitted from the documentation of :unmark. This
commit adds the documentation for this flag to the :unmark command.

Signed-off-by: Moritz Poldrack <git@moritz.sh>
---
 doc/aerc.1.scd | 6 ++++--
 1 file changed, 4 insertions(+), 2 deletions(-)

diff --git a/doc/aerc.1.scd b/doc/aerc.1.scd
index 233a1139..595a94af 100644
--- a/doc/aerc.1.scd
+++ b/doc/aerc.1.scd
@@ -861,14 +861,16 @@ message list, the message in the message viewer, etc).

	*-r*: apply the filter to the To: header (does not work with *-v* or *-V*)

*:unmark* [*-atsr*] _<filter>_
*:unmark* [*-atTsr*] _<filter>_
	Unmarks messages. The flags below can be combined as needed. The
	existence of a filter implies *-a*.
	existence of a filter implies *-a* unless *-T* has been specified.

	*-a*: Apply to all messages in the current folder

	*-t*: toggle the mark state instead of unmarking a message

	*-T*: Marks the displayed message thread of the selected message.

	*-s*: apply the filter to the From: header (does not work with *-v* or *-V*)

	*-r*: apply the filter to the To: header (does not work with *-v* or *-V*)
-- 
2.48.1

[PATCH aerc v4 3/3] commands/mark: remove unsupported -v and -V flags from suggestions

Moritz Poldrack <git@moritz.sh>
Details
Message ID
<20250218153834.18779-3-git@moritz.sh>
In-Reply-To
<20250218153834.18779-1-git@moritz.sh> (view parent)
Sender timestamp
1739896662
DKIM signature
pass
Download raw message
Patch: +2 -2
The -v and -V flags are unsupported/nonsensical for the :unmark command,
but are still suggested in completions. Remove them.

Signed-off-by: Moritz Poldrack <git@moritz.sh>
---
 commands/msg/mark.go | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/commands/msg/mark.go b/commands/msg/mark.go
index b71357da..f106bb6a 100644
--- a/commands/msg/mark.go
+++ b/commands/msg/mark.go
@@ -16,8 +16,8 @@ import (
type Mark struct {
	All             bool   `opt:"-a" aliases:"mark,unmark" desc:"Mark all messages in current folder."`
	Toggle          bool   `opt:"-t" aliases:"mark,unmark" desc:"Toggle the marked state."`
	Visual          bool   `opt:"-v" aliases:"mark,unmark" desc:"Enter / leave visual mark mode."`
	VisualClear     bool   `opt:"-V" aliases:"mark,unmark" desc:"Same as -v but does not clear existing selection."`
	Visual          bool   `opt:"-v" aliases:"mark" desc:"Enter / leave visual mark mode."`
	VisualClear     bool   `opt:"-V" aliases:"mark" desc:"Same as -v but does not clear existing selection."`
	Thread          bool   `opt:"-T" aliases:"mark,unmark" desc:"Mark all messages from the selected thread."`
	SenderFilter    bool   `opt:"-s" aliases:"mark,unmark" desc:"Mark all messages sent from a sender containing the given substring."`
	RecipientFilter bool   `opt:"-r" aliases:"mark,unmark" desc:"Mark all messages received from an address containing the given substring."`
-- 
2.48.1
Details
Message ID
<D7VOXBZNPGAH.2EBLHNUU5K1GE@sindominio.net>
In-Reply-To
<20250218153834.18779-1-git@moritz.sh> (view parent)
Sender timestamp
1739897536
DKIM signature
pass
Download raw message
Nice addition!

Tested-By: inwit <inwit@sindominio.net>
Reply to thread Export thread (mbox)