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
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