Allow configuration of maildir flags so people can use their own tag
names and allow additional flags as used by dovecot keywords.

As this allows to use more flags than the current 5 use indexes to
lookup the flags and tags.

These indexes are allocated static so there's no need to malloc and
free. The indexes are created lazy and not done when flag synchonization
is disabled.

I decided to use a ;-separated list for the configuration so a default
can be set by the configuration.

For simplicity when duplicate flags/tags are specified the first value
is used.
---
 contrib/python-legacy/notmuch/message.py      |  14 --
 doc/man1/notmuch-config.rst                   |  23 +--
 lib/config.cc                                 |   4 +
 lib/message.cc                                | 189 ++++++++++++++----
 lib/notmuch.h                                 |  26 +--
 notmuch-config.c                              |  14 +-
 test/T030-config.sh                           |   1 +
 test/T055-path-config.sh                      |   2 +
 test/T590-libconfig.sh                        |  70 ++++---
 .../config-with-comments                      |  14 +-
 10 files changed, 224 insertions(+), 133 deletions(-)

diff --git a/contrib/python-legacy/notmuch/message.py 
b/contrib/python-legacy/notmuch/message.py
index e71dbe3e9..4e7cf7f37 100644
--- a/contrib/python-legacy/notmuch/message.py
+++ b/contrib/python-legacy/notmuch/message.py
@@ -606,12 +606,6 @@ class Message(Python3StringMixIn):
     def tags_to_maildir_flags(self):
         """Synchronize notmuch tags to file Maildir flags
 
-              'D' if the message has the "draft" tag
-              'F' if the message has the "flagged" tag
-              'P' if the message has the "passed" tag
-              'R' if the message has the "replied" tag
-              'S' if the message does not have the "unread" tag
-
         Any existing flags unmentioned in the list above will be
         preserved in the renaming.
 
@@ -631,14 +625,6 @@ class Message(Python3StringMixIn):
     def maildir_flags_to_tags(self):
         """Synchronize file Maildir flags to notmuch tags
 
-            Flag    Action if present
-            ----    -----------------
-            'D'     Adds the "draft" tag to the message
-            'F'     Adds the "flagged" tag to the message
-            'P'     Adds the "passed" tag to the message
-            'R'     Adds the "replied" tag to the message
-            'S'     Removes the "unread" tag from the message
-
         For each flag that is not present, the opposite action
         (add/remove) is performed for the corresponding tags.  If there
         are multiple filenames associated with this message, the flag is
diff --git a/doc/man1/notmuch-config.rst b/doc/man1/notmuch-config.rst
index 6a63e4570..93c5501e6 100644
--- a/doc/man1/notmuch-config.rst
+++ b/doc/man1/notmuch-config.rst
@@ -223,21 +223,7 @@ paths are presumed relative to `$HOME` for items in section
 .. nmconfig:: maildir.synchronize_flags
 
     If true, then the following maildir flags (in message filenames)
-    will be synchronized with the corresponding notmuch tags:
-
-    +--------+-----------------------------------------------+
-    | Flag   | Tag                                           |
-    +========+===============================================+
-    | D      | draft                                         |
-    +--------+-----------------------------------------------+
-    | F      | flagged                                       |
-    +--------+-----------------------------------------------+
-    | P      | passed                                        |
-    +--------+-----------------------------------------------+
-    | R      | replied                                       |
-    +--------+-----------------------------------------------+
-    | S      | unread (added when 'S' flag is not present)   |
-    +--------+-----------------------------------------------+
+    will be synchronized with the corresponding notmuch tags.
 
     The :any:`notmuch-new(1)` command will notice flag changes in
     filenames and update tags, while the :any:`notmuch-tag(1)` and
@@ -254,6 +240,13 @@ paths are presumed relative to `$HOME` for items in section
 
     Default: ``true``.
 
+.. nmconfig:: maildir.flags
+
+    A list of maildir flags to tags mappings. When the flag is prefixed
+    with a caret (^) the tagging is inversed.
+
+    Default: ``D:draft;F:flagged;P:passed;R:replied;^S:unread``
+
 .. nmconfig:: new.ignore
 
     A list to specify files and directories that will not be searched
diff --git a/lib/config.cc b/lib/config.cc
index d231c8931..d3597cb0b 100644
--- a/lib/config.cc
+++ b/lib/config.cc
@@ -592,6 +592,8 @@ _notmuch_config_key_to_string (notmuch_config_key_t key)
        return "new.ignore";
     case NOTMUCH_CONFIG_SYNC_MAILDIR_FLAGS:
        return "maildir.synchronize_flags";
+    case NOTMUCH_CONFIG_MAILDIR_FLAGS:
+       return "maildir.flags";
     case NOTMUCH_CONFIG_PRIMARY_EMAIL:
        return "user.primary_email";
     case NOTMUCH_CONFIG_OTHER_EMAIL:
@@ -644,6 +646,8 @@ _notmuch_config_default (notmuch_database_t *notmuch, 
notmuch_config_key_t key)
     case NOTMUCH_CONFIG_SYNC_MAILDIR_FLAGS:
     case NOTMUCH_CONFIG_GIT_FAIL_ON_MISSING:
        return "true";
+    case NOTMUCH_CONFIG_MAILDIR_FLAGS:
+       return "D:draft;F:flagged;P:passed;R:replied;^S:unread";
     case NOTMUCH_CONFIG_USER_NAME:
        name = getenv ("NAME");
        if (name)
diff --git a/lib/message.cc b/lib/message.cc
index 8b1fa400e..aec0e3e00 100644
--- a/lib/message.cc
+++ b/lib/message.cc
@@ -59,22 +59,126 @@ struct _notmuch_message {
     Xapian::termcount termpos;
 };
 
-#define ARRAY_SIZE(arr) (sizeof (arr) / sizeof (arr[0]))
-
 struct maildir_flag_tag {
     char flag;
-    const char *tag;
+    char *tag;
     bool inverse;
 };
 
-/* ASCII ordered table of Maildir flags and associated tags */
-static const struct maildir_flag_tag flag2tag[] = {
-    { 'D', "draft",   false },
-    { 'F', "flagged", false },
-    { 'P', "passed",  false },
-    { 'R', "replied", false },
-    { 'S', "unread",  true }
-};
+/* Direct lookup table for maildir flag characters uppercase A-Z
+ * (indices 0-25) and lowercase a-z (indices 26-51). */
+static struct maildir_flag_tag maildir_flag_tags[53];
+
+
+/* Sorted index array into maildir_flag_tags for bsearch.
+ * tag2flag[i] is an index into maildir_flag_tags (0..51); the
+ * array is sorted by maildir_flag_tags[tag2flag[i]].tag so
+ * that bsearch on a tag yields the corresponding index. */
+
+#define N0(x)  (x), (x)+1, (x)+2, (x)+3, (x)+4, (x)+5, (x)+6, (x)+7, (x)+8, 
(x)+9
+#define N50(x) N0(x), N0((x)+10), N0((x)+20), N0((x)+30), N0((x)+40)
+
+static uint8_t tag2flag[52] = { N50(0), 50, 51 };
+static bool _maildir_flag_tags_initialized = false;
+
+static int
+_cmp_tag2flag (const void *key, const void *elem)
+{
+    const char *tag = (const char *) key;
+    int idx = *(const uint8_t *) elem;
+    const char *entry_tag = maildir_flag_tags[idx].tag;
+
+    /* NULL tags sort after all valid tags; search key is always
+     * less than NULL. */
+    if (entry_tag == NULL)
+       return -1;
+    return strcmp (tag, entry_tag);
+}
+
+static int
+_cmp_tag2flag_sort (const void *a, const void *b)
+{
+    int ia = *(const uint8_t *) a;
+    int ib = *(const uint8_t *) b;
+    const char *ta = maildir_flag_tags[ia].tag;
+    const char *tb = maildir_flag_tags[ib].tag;
+
+    if (ta == NULL && tb == NULL)
+       return 0;
+    if (ta == NULL)
+       return 1;
+    if (tb == NULL)
+       return -1;
+    return strcmp (ta, tb);
+}
+
+static void
+_init_maildir_flag_tags (notmuch_database_t *notmuch)
+{
+    const char *config_str;
+    const char *tok;
+    size_t tok_len;
+
+    if (_maildir_flag_tags_initialized)
+       return;
+
+    memset (maildir_flag_tags, 0, sizeof (maildir_flag_tags));
+
+    config_str = notmuch_config_get (notmuch, NOTMUCH_CONFIG_MAILDIR_FLAGS);
+
+    /* Parse and populate maildir_flag_tags */
+    tok = config_str;
+    tok_len = 0;
+    while ((tok = strtok_len_c (tok + tok_len, ";", &tok_len)) != NULL) {
+       bool inverse = false;
+       char flag_char;
+       const char *colon;
+       int idx;
+       const char *p = tok;
+       const char *end = tok + tok_len;
+
+       /* Skip leading whitespace */
+       while (p < end && (*p == ' ' || *p == '\t'))
+           p++;
+
+       if (p >= end)
+           continue;
+
+       if (*p == '^') {
+           inverse = true;
+           p++;
+       }
+
+       if (p >= end)
+           continue;
+
+       flag_char = *p;
+
+       colon = (const char *) memchr (p, ':', end - p);
+       if (! colon || colon + 1 >= end)
+           continue;
+
+       /* Calculate array index for maildir_flag_tags */
+       if (flag_char >= 'A' && flag_char <= 'Z')
+           idx = flag_char - 'A';
+       else if (flag_char >= 'a' && flag_char <= 'z')
+           idx = flag_char - 'a' + 26;
+       else
+           continue;
+
+       /* Populate maildir_flag_tags for direct flag lookup */
+       maildir_flag_tags[idx].flag = flag_char;
+       maildir_flag_tags[idx].tag = talloc_strndup (notmuch, colon + 1,
+                                                     end - (colon + 1));
+       maildir_flag_tags[idx].inverse = inverse;
+    }
+
+    /* Sort tag2flag indices by the tag values in maildir_flag_tags;
+     * NULL-tag entries come after all valid-tag entries. */
+    qsort (tag2flag, 52, sizeof (uint8_t), _cmp_tag2flag_sort);
+
+    _maildir_flag_tags_initialized = true;
+}
 
 /* We end up having to call the destructor explicitly because we had
  * to use "placement new" in order to initialize C++ objects within a
@@ -1857,13 +1961,23 @@ notmuch_message_maildir_flags_to_tags 
(notmuch_message_t *message)
     if (status)
        return status;
 
-    for (i = 0; i < ARRAY_SIZE (flag2tag); i++) {
-       if ((strchr (message->maildir_flags, flag2tag[i].flag) != NULL)
+    _init_maildir_flag_tags (message->notmuch);
+
+    /* For each configured flag, check presence in maildir_flags
+     * and apply the corresponding tag action. */
+    for (i = 0;
+        i < 52 && maildir_flag_tags[tag2flag[i]].tag != NULL; i++) {
+       int idx = tag2flag[i];
+
+       if ((strchr (message->maildir_flags,
+                    maildir_flag_tags[idx].flag) != NULL)
            ^
-           flag2tag[i].inverse) {
-           status = notmuch_message_add_tag (message, flag2tag[i].tag);
+           maildir_flag_tags[idx].inverse) {
+           status = notmuch_message_add_tag (message,
+                                             maildir_flag_tags[idx].tag);
        } else {
-           status = notmuch_message_remove_tag (message, flag2tag[i].tag);
+           status = notmuch_message_remove_tag (message,
+                                                maildir_flag_tags[idx].tag);
        }
        if (status)
            return status;
@@ -1873,7 +1987,7 @@ notmuch_message_maildir_flags_to_tags (notmuch_message_t 
*message)
     return status;
 }
 
-/* From the set of tags on 'message' and the flag2tag table, compute a
+/* From the set of tags on 'message' and the tag2flag table, compute a
  * set of maildir-flag actions to be taken, (flags that should be
  * either set or cleared).
  *
@@ -1889,6 +2003,8 @@ _get_maildir_flag_actions (notmuch_message_t *message,
     const char *tag;
     unsigned i;
 
+    _init_maildir_flag_tags (message->notmuch);
+
     to_set = talloc_strdup (message, "");
     to_clear = talloc_strdup (message, "");
 
@@ -1898,28 +2014,33 @@ _get_maildir_flag_actions (notmuch_message_t *message,
         notmuch_tags_move_to_next (tags)) {
        tag = notmuch_tags_get (tags);
 
-       for (i = 0; i < ARRAY_SIZE (flag2tag); i++) {
-           if (strcmp (tag, flag2tag[i].tag) == 0) {
-               if (flag2tag[i].inverse)
-                   to_clear = talloc_asprintf_append (to_clear,
-                                                      "%c",
-                                                      flag2tag[i].flag);
-               else
-                   to_set = talloc_asprintf_append (to_set,
-                                                    "%c",
-                                                    flag2tag[i].flag);
-           }
+       const uint8_t *match_idx;
+       match_idx = (const uint8_t *)
+           bsearch (tag, tag2flag, 52,
+                    sizeof (uint8_t), _cmp_tag2flag);
+       if (match_idx && maildir_flag_tags[*match_idx].tag != NULL) {
+           if (maildir_flag_tags[*match_idx].inverse)
+               to_clear = talloc_asprintf_append (to_clear, "%c",
+                                                  
maildir_flag_tags[*match_idx].flag);
+           else
+               to_set = talloc_asprintf_append (to_set, "%c",
+                                                
maildir_flag_tags[*match_idx].flag);
        }
     }
 
     /* Then, find the flags for all tags not present. */
-    for (i = 0; i < ARRAY_SIZE (flag2tag); i++) {
-       if (flag2tag[i].inverse) {
-           if (strchr (to_clear, flag2tag[i].flag) == NULL)
-               to_set = talloc_asprintf_append (to_set, "%c", 
flag2tag[i].flag);
+    for (i = 0;
+        i < 52 && maildir_flag_tags[tag2flag[i]].tag != NULL; i++) {
+       int idx = tag2flag[i];
+
+       if (maildir_flag_tags[idx].inverse) {
+           if (strchr (to_clear, maildir_flag_tags[idx].flag) == NULL)
+               to_set = talloc_asprintf_append (to_set, "%c",
+                                                maildir_flag_tags[idx].flag);
        } else {
-           if (strchr (to_set, flag2tag[i].flag) == NULL)
-               to_clear = talloc_asprintf_append (to_clear, "%c", 
flag2tag[i].flag);
+           if (strchr (to_set, maildir_flag_tags[idx].flag) == NULL)
+               to_clear = talloc_asprintf_append (to_clear, "%c",
+                                                  maildir_flag_tags[idx].flag);
        }
     }
 
diff --git a/lib/notmuch.h b/lib/notmuch.h
index c403a3480..ef043c607 100644
--- a/lib/notmuch.h
+++ b/lib/notmuch.h
@@ -1949,16 +1949,8 @@ notmuch_message_remove_all_tags (notmuch_message_t 
*message);
  * Add/remove tags according to maildir flags in the message filename(s).
  *
  * This function examines the filenames of 'message' for maildir
- * flags, and adds or removes tags on 'message' as follows when these
- * flags are present:
- *
- *     Flag    Action if present
- *     ----    -----------------
- *     'D'     Adds the "draft" tag to the message
- *     'F'     Adds the "flagged" tag to the message
- *     'P'     Adds the "passed" tag to the message
- *     'R'     Adds the "replied" tag to the message
- *     'S'     Removes the "unread" tag from the message
+ * flags, and adds or removes tags on 'message' as configured by
+ * maildir.flags.
  *
  * For each flag that is not present, the opposite action (add/remove)
  * is performed for the corresponding tags.
@@ -2023,16 +2015,11 @@ notmuch_message_has_maildir_flag_st (notmuch_message_t 
*message,
  *
  * If the filename is in a maildir directory, rename the file so that
  * its filename ends with the sequence ":2," followed by zero or more
- * of the following single-character flags (in ASCII order):
- *
- *   * flag 'D' iff the message has the "draft" tag
- *   * flag 'F' iff the message has the "flagged" tag
- *   * flag 'P' iff the message has the "passed" tag
- *   * flag 'R' iff the message has the "replied" tag
- *   * flag 'S' iff the message does not have the "unread" tag
+ * single-character flags (in ASCII order) as configured by
+ * maildir.flags.
  *
- * Any existing flags unmentioned in the list above will be preserved
- * in the renaming.
+ * Any existing flags unmentioned in the configuration will be
+ * preserved in the renaming.
  *
  * Also, if this filename is in a directory named "new", rename it to
  * be within the neighboring directory named "cur".
@@ -2633,6 +2620,7 @@ typedef enum {
     NOTMUCH_CONFIG_NEW_TAGS,
     NOTMUCH_CONFIG_NEW_IGNORE,
     NOTMUCH_CONFIG_SYNC_MAILDIR_FLAGS,
+    NOTMUCH_CONFIG_MAILDIR_FLAGS,
     NOTMUCH_CONFIG_PRIMARY_EMAIL,
     NOTMUCH_CONFIG_OTHER_EMAIL,
     NOTMUCH_CONFIG_USER_NAME,
diff --git a/notmuch-config.c b/notmuch-config.c
index f7e59f1ea..1112621a5 100644
--- a/notmuch-config.c
+++ b/notmuch-config.c
@@ -101,19 +101,15 @@ static const struct config_group {
        "\tsynchronize_flags      Valid values are true and false.\n"
        "\n"
        "\tIf true, then the following maildir flags (in message filenames)\n"
-       "\twill be synchronized with the corresponding notmuch tags:\n"
-       "\n"
-       "\t\tFlag       Tag\n"
-       "\t\t----       -------\n"
-       "\t\tD  draft\n"
-       "\t\tF  flagged\n"
-       "\t\tP  passed\n"
-       "\t\tR  replied\n"
-       "\t\tS  unread (added when 'S' flag is not present)\n"
+       "\twill be synchronized with the corresponding notmuch tags.\n"
        "\n"
        "\tThe \"notmuch new\" command will notice flag changes in filenames\n"
        "\tand update tags, while the \"notmuch tag\" and \"notmuch restore\"\n"
        "\tcommands will notice tag changes and update flags in filenames\n"
+       "\n"
+       "\tflags\n"
+       "\t\tA ;-separated list of flag tags mapping. Flags prefixed with a 
caret\n"
+       "\twill invert the tagging.\n"
     },
 };
 
diff --git a/test/T030-config.sh b/test/T030-config.sh
index ce6653ee2..e1ccccb91 100755
--- a/test/T030-config.sh
+++ b/test/T030-config.sh
@@ -63,6 +63,7 @@ git.fail_on_missing=true
 git.metadata_prefix=_notmuch_metadata
 git.ref=refs/heads/master
 index.as_text=
+maildir.flags=D:draft;F:flagged;P:passed;R:replied;^S:unread
 maildir.synchronize_flags=true
 new.ignore=
 new.tags=unread;inbox
diff --git a/test/T055-path-config.sh b/test/T055-path-config.sh
index 4d1edec0e..cbc21274f 100755
--- a/test/T055-path-config.sh
+++ b/test/T055-path-config.sh
@@ -303,6 +303,7 @@ git.fail_on_missing=true
 git.metadata_prefix=_notmuch_metadata
 git.ref=refs/heads/master
 index.as_text=
+maildir.flags=D:draft;F:flagged;P:passed;R:replied;^S:unread
 maildir.synchronize_flags=true
 new.ignore=
 new.tags=unread;inbox
@@ -331,6 +332,7 @@ database.path
 git.fail_on_missing
 git.metadata_prefix
 git.ref
+maildir.flags
 maildir.synchronize_flags
 new.tags
 search.authors_matched_separator
diff --git a/test/T590-libconfig.sh b/test/T590-libconfig.sh
index e893fd1f4..206f2c43a 100755
--- a/test/T590-libconfig.sh
+++ b/test/T590-libconfig.sh
@@ -435,17 +435,18 @@ cat <<'EOF' >EXPECTED
 05: 'unread;inbox'
 06: ''
 07: 'true'
-08: 'USERNAME@localhost'
-09: 'NULL'
-10: 'USER_FULL_NAME'
-11: '8000'
-12: 'NULL'
-13: ''
-14: ', '
-15: '| '
-16: 'true'
-17: '_notmuch_metadata'
-18: 'refs/heads/master'
+08: 'D:draft;F:flagged;P:passed;R:replied;^S:unread'
+09: 'USERNAME@localhost'
+10: 'NULL'
+11: 'USER_FULL_NAME'
+12: '8000'
+13: 'NULL'
+14: ''
+15: ', '
+16: '| '
+17: 'true'
+18: '_notmuch_metadata'
+19: 'refs/heads/master'
 == stderr ==
 EOF
 unset MAILDIR
@@ -755,17 +756,18 @@ cat <<'EOF' >EXPECTED
 05: 'unread;inbox'
 06: 'sekrit_junk'
 07: 'true'
-08: '[email protected]'
-09: '[email protected];[email protected]'
-10: 'Notmuch Test Suite'
-11: '8000'
-12: 'NULL'
-13: 'text/'
-14: ', '
-15: '| '
-16: 'true'
-17: '_notmuch_metadata'
-18: 'refs/heads/master'
+08: 'D:draft;F:flagged;P:passed;R:replied;^S:unread'
+09: '[email protected]'
+10: '[email protected];[email protected]'
+11: 'Notmuch Test Suite'
+12: '8000'
+13: 'NULL'
+14: 'text/'
+15: ', '
+16: '| '
+17: 'true'
+18: '_notmuch_metadata'
+19: 'refs/heads/master'
 == stderr ==
 EOF
 test_expect_equal_file EXPECTED OUTPUT
@@ -795,17 +797,18 @@ cat <<'EOF' >EXPECTED
 05: 'unread;inbox'
 06: ''
 07: 'true'
-08: 'USERNAME@localhost'
-09: 'NULL'
-10: 'USER_FULL_NAME'
-11: '8000'
-12: 'NULL'
-13: ''
-14: ', '
-15: '| '
-16: 'true'
-17: '_notmuch_metadata'
-18: 'refs/heads/master'
+08: 'D:draft;F:flagged;P:passed;R:replied;^S:unread'
+09: 'USERNAME@localhost'
+10: 'NULL'
+11: 'USER_FULL_NAME'
+12: '8000'
+13: 'NULL'
+14: ''
+15: ', '
+16: '| '
+17: 'true'
+18: '_notmuch_metadata'
+19: 'refs/heads/master'
 == stderr ==
 EOF
 test_expect_equal_file EXPECTED OUTPUT.clean
@@ -882,6 +885,7 @@ git.metadata_prefix _notmuch_metadata
 git.ref refs/heads/master
 index.as_text text/
 key with spaces value, with, spaces!
+maildir.flags D:draft;F:flagged;P:passed;R:replied;^S:unread
 maildir.synchronize_flags true
 new.ignore sekrit_junk
 new.tags unread;inbox
diff --git a/test/setup.expected-output/config-with-comments 
b/test/setup.expected-output/config-with-comments
index 5d96be7f4..573d13eb8 100644
--- a/test/setup.expected-output/config-with-comments
+++ b/test/setup.expected-output/config-with-comments
@@ -66,18 +66,14 @@ exclude_tags=baz
 #      synchronize_flags      Valid values are true and false.
 #
 #      If true, then the following maildir flags (in message filenames)
-#      will be synchronized with the corresponding notmuch tags:
-#
-#              Flag    Tag
-#              ----    -------
-#              D       draft
-#              F       flagged
-#              P       passed
-#              R       replied
-#              S       unread (added when 'S' flag is not present)
+#      will be synchronized with the corresponding notmuch tags.
 #
 #      The "notmuch new" command will notice flag changes in filenames
 #      and update tags, while the "notmuch tag" and "notmuch restore"
 #      commands will notice tag changes and update flags in filenames
 #
+#      flags
+#              A ;-separated list of flag tags mapping. Flags prefixed with a 
caret
+#      will invert the tagging.
+#
 [maildir]
-- 
2.54.0

_______________________________________________
notmuch mailing list -- [email protected]
To unsubscribe send an email to [email protected]

Reply via email to