]> granicus.if.org Git - neomutt/commitdiff
feature: keywords
authorDavid Champion <dgc@bikeshed.us>
Fri, 8 Apr 2016 00:01:50 +0000 (01:01 +0100)
committerRichard Russon <rich@flatcap.org>
Thu, 18 Aug 2016 15:15:20 +0000 (16:15 +0100)
28 files changed:
OPS
commands.c
copy.c
copy.h
curs_main.c
enter.c
functions.h
globals.h
hash.c
hash.h
hcache.c
hdrline.c
headers.c
imap/imap.c
imap/message.c
init.c
init.h
main.c
mh.c
mutt.h
muttlib.c
pager.c
parse.c
pattern.c
protos.h
sendlib.c
sort.c
sort.h

diff --git a/OPS b/OPS
index 02cea8e1e86293ead331873f2fe9b60783dc5acc..43d26c1e9f8023b21ee0cea3fd4eb9b5fe5f5378 100644 (file)
--- a/OPS
+++ b/OPS
@@ -56,6 +56,7 @@ OP_DELETE_THREAD "delete all messages in thread"
 OP_DISPLAY_ADDRESS "display full address of sender"
 OP_DISPLAY_HEADERS "display message and toggle header weeding"
 OP_DISPLAY_MESSAGE "display a message"
+OP_EDIT_LABEL "add, change, or delete a message's label"
 OP_EDIT_MESSAGE "edit the raw message"
 OP_EDITOR_BACKSPACE "delete the char in front of the cursor"
 OP_EDITOR_BACKWARD_CHAR "move the cursor one character to the left"
index 2202a673328ec3ba027525f5f0e569da0133da8a..e734bbfed7db16712fb6ab2523cec0ae798600ea 100644 (file)
@@ -533,9 +533,9 @@ int mutt_select_sort (int reverse)
   int method = Sort; /* save the current method in case of abort */
 
   switch (mutt_multi_choice (reverse ?
-                            _("Rev-Sort (d)ate/(f)rm/(r)ecv/(s)ubj/t(o)/(t)hread/(u)nsort/si(z)e/s(c)ore/s(p)am?: ") :
-                            _("Sort (d)ate/(f)rm/(r)ecv/(s)ubj/t(o)/(t)hread/(u)nsort/si(z)e/s(c)ore/s(p)am?: "),
-                            _("dfrsotuzcp")))
+                            _("Rev-Sort Date/Frm/Recv/Subj/tO/Thread/Unsort/siZe/sCore/sPam/Label?: ") :
+                            _("Sort Date/Frm/Recv/Subj/tO/Thread/Unsort/siZe/sCore/sPam/Label?: "),
+                            _("dfrsotuzcpl")))
   {
   case -1: /* abort - don't resort */
     return -1;
@@ -579,6 +579,10 @@ int mutt_select_sort (int reverse)
   case 10: /* s(p)am */
     Sort = SORT_SPAM;
     break;
+
+  case 11: /* (l)abel */
+    Sort = SORT_LABEL;
+    break;
   }
   if (reverse)
     Sort |= SORT_REVERSE;
diff --git a/copy.c b/copy.c
index 1ef441f44eb2842a386be6cf86acf67c6602fd55..5f938b328ef68ea2c559feb50cf21ac6fc477e3b 100644 (file)
--- a/copy.c
+++ b/copy.c
@@ -111,6 +111,15 @@ mutt_copy_hdr (FILE *in, FILE *out, LOFF_T off_start, LOFF_T off_end, int flags,
        ignore = 0;
       }
 
+      if (flags & CH_UPDATE_LABEL)
+      {
+       if ((mutt_strncasecmp ("X-Label:", buf, 8) == 0) ||
+           (mutt_strncasecmp ("X-Keywords:", buf, 11) == 0) ||
+           (mutt_strncasecmp ("X-Mozilla-Keys:", buf, 15) == 0) ||
+           (mutt_strncasecmp ("Keywords:", buf, 9) == 0))
+         continue;
+      }
+
       if (!ignore && fputs (buf, out) == EOF)
        return (-1);
     }
@@ -414,6 +423,61 @@ mutt_copy_header (FILE *in, HEADER *h, FILE *out, int flags, const char *prefix)
       fprintf (out, "Lines: %d\n", h->lines);
   }
 
+  if (flags & CH_UPDATE_LABEL && h->label_changed)
+  {
+    h->label_changed = 0;
+    if (h->env->labels != NULL)
+    {
+      char buf[HUGE_STRING];
+      char *tmp = NULL;
+      int fail = 0;
+
+      if (fail == 0 &&
+          ((h->env->kwtypes & MUTT_X_LABEL) || (h->env->kwtypes == 0)) &&
+          (option(OPTKEYWORDSLEGACY) || option(OPTKEYWORDSSTANDARD) == 0))
+      {
+        mutt_labels(buf, sizeof(buf), h->env, XlabelDelim);
+        tmp = safe_strdup(buf);
+        rfc2047_encode_string(&tmp);
+        fail = fprintf(out, "X-Label: %s\n", tmp) != 10 + strlen(tmp);
+        FREE(&tmp);
+      }
+
+      if (fail == 0 && (h->env->kwtypes & MUTT_X_KEYWORDS) &&
+          (option(OPTKEYWORDSLEGACY) || option(OPTKEYWORDSSTANDARD) == 0))
+      {
+        mutt_labels(buf, sizeof(buf), h->env, " ");
+        tmp = safe_strdup(buf);
+        rfc2047_encode_string(&tmp);
+        fail = fprintf(out, "X-Keywords: %s\n", tmp) != 13 + strlen(tmp);
+        FREE(&tmp);
+      }
+
+      if (fail == 0 && (h->env->kwtypes & MUTT_X_MOZILLA_KEYS) &&
+          (option(OPTKEYWORDSLEGACY) || option(OPTKEYWORDSSTANDARD) == 0))
+      {
+        mutt_labels(buf, sizeof(buf), h->env, " ");
+        tmp = safe_strdup(buf);
+        rfc2047_encode_string(&tmp);
+        fail = fprintf(out, "X-Mozilla-Keys: %s\n", tmp) != 17 + strlen(tmp);
+        FREE(&tmp);
+      }
+
+      if (fail == 0 && ((h->env->kwtypes & MUTT_KEYWORDS) ||
+                        option(OPTKEYWORDSSTANDARD)))
+      {
+        mutt_labels(buf, sizeof(buf), h->env, NULL);
+        tmp = safe_strdup(buf);
+        rfc2047_encode_string(&tmp);
+        fail = fprintf(out, "Keywords: %s\n", tmp) != 11 + strlen(tmp);
+        FREE(&tmp);
+      }
+
+      if (fail)
+        return -1;
+    }
+  }
+
   if ((flags & CH_NONEWLINE) == 0)
   {
     if (flags & CH_PREFIX)
@@ -494,6 +558,9 @@ _mutt_copy_message (FILE *fpout, FILE *fpin, HEADER *hdr, BODY *body,
       _mutt_make_string (prefix, sizeof (prefix), NONULL (Prefix), Context, hdr, 0);
   }
 
+  if (hdr->label_changed)
+    chflags |= CH_UPDATE_LABEL;
+
   if ((flags & MUTT_CM_NOHEADER) == 0)
   {
     if (flags & MUTT_CM_PREFIX)
diff --git a/copy.h b/copy.h
index 4d6fc54a7a8ba51aa336b450faa6cf115b41c212..2ba54143fc856dcdb44b676eec7ec1133b064196 100644 (file)
--- a/copy.h
+++ b/copy.h
@@ -53,6 +53,7 @@
 #define CH_UPDATE_IRT     (1<<16) /* update In-Reply-To: */
 #define CH_UPDATE_REFS    (1<<17) /* update References: */
 #define CH_DISPLAY        (1<<18) /* display result to user */
+#define CH_UPDATE_LABEL   (1<<19) /* update X-Label: from hdr->env->x_label? */
 
 
 int mutt_copy_hdr (FILE *, FILE *, LOFF_T, LOFF_T, int, const char *);
index 8e0f52ad837eadbee37601851c2626956f4e9e01..924382af318013e158e1f09ee16c0218be6aabf1 100644 (file)
@@ -1256,6 +1256,9 @@ int mutt_index_menu (void)
          FREE (&Context);
        }
 
+        if (Labels)
+          hash_destroy(&Labels, NULL);
+
         mutt_sleep (0);
 
        /* Set CurrentMenu to MENU_MAIN before executing any folder
@@ -1270,6 +1273,8 @@ int mutt_index_menu (void)
                                        (option (OPTREADONLY) || op == OP_MAIN_CHANGE_FOLDER_READONLY) ?
                                        MUTT_READONLY : 0, NULL)) != NULL)
        {
+         Labels = hash_create(131, 0);
+         mutt_scan_labels(Context);
          menu->current = ci_first_message ();
        }
        else
@@ -2079,6 +2084,21 @@ int mutt_index_menu (void)
        menu->redraw = REDRAW_FULL;
        break;
 
+      case OP_EDIT_LABEL:
+
+       CHECK_MSGCOUNT;
+       CHECK_READONLY;
+       rc = mutt_label_message(tag ? NULL : CURHDR);
+       if (rc > 0) {
+         Context->changed = 1;
+         menu->redraw = REDRAW_FULL;
+         mutt_message ("%d label%s changed.", rc, rc == 1 ? "" : "s");
+       }
+       else {
+         mutt_message _("No labels changed.");
+       }
+       break;
+
       case OP_LIST_REPLY:
 
        CHECK_ATTACH;
diff --git a/enter.c b/enter.c
index 2f3ebdd5d4b5912fb08a49b4bcb2f1ec7438e801..a14a34777087b1d6330e01a2fdc2204ab683b197 100644 (file)
--- a/enter.c
+++ b/enter.c
@@ -566,6 +566,49 @@ int _mutt_enter_string (char *buf, size_t buflen, int col,
            }
            break;
          }
+         else if (flags & MUTT_LABEL && ch == OP_EDITOR_COMPLETE)
+         {
+           for (i = state->curpos; i && state->wbuf[i-1] != ',' && 
+                state->wbuf[i-1] != ':'; i--)
+             ;
+           for (; i < state->lastchar && state->wbuf[i] == ' '; i++)
+             ;
+           my_wcstombs (buf, buflen, state->wbuf + i, state->curpos - i);
+           r = mutt_label_complete (buf, buflen, i, state->tabs);
+           replace_part (state, i, buf);
+           if (!r)
+           {
+             rv = 1;
+             goto bye;
+           }
+           break;
+         }
+         else if (flags & MUTT_PATTERN && ch == OP_EDITOR_COMPLETE)
+         {
+        char *p;
+           for (i = state->curpos; i && state->wbuf[i-1] != ',' && 
+                state->wbuf[i-1] != ':'; i--)
+             ;
+           for (; i < state->lastchar && state->wbuf[i] == ' '; i++)
+             ;
+           my_wcstombs (buf, buflen, state->wbuf + i, state->curpos - i);
+        p = &buf[i];
+        while (p > buf && *(p-1) != '~')
+          p--;
+        if (*p == '~' && *(p+1) == 'y')
+        {
+             r = mutt_label_complete (buf, buflen, i, state->tabs);
+             replace_part (state, i, buf);
+             if (!r)
+             {
+               rv = 1;
+               goto bye;
+             }
+        }
+        else
+          goto self_insert;
+           break;
+         }
          else if (flags & MUTT_ALIAS && ch == OP_EDITOR_COMPLETE_QUERY)
          {
            /* invoke the query-menu to get more addresses */
index abc06ca0da943c40a69545a52a46dc4770b1f6d7..87fe7b6b6bc81dbac6331da67496ded3d5eacdae 100644 (file)
@@ -99,6 +99,7 @@ const struct binding_t OpMain[] = { /* map: index */
   { "delete-thread",           OP_DELETE_THREAD,               "\004" },
   { "delete-subthread",                OP_DELETE_SUBTHREAD,            "\033d" },
   { "edit",                    OP_EDIT_MESSAGE,                "e" },
+  { "edit-label",              OP_EDIT_LABEL,                  NULL },
   { "edit-type",               OP_EDIT_TYPE,                   "\005" },
   { "forward-message",         OP_FORWARD_MESSAGE,             "f" },
   { "flag-message",            OP_FLAG_MESSAGE,                "F" },
@@ -198,6 +199,7 @@ const struct binding_t OpPager[] = { /* map: pager */
   { "set-flag",        OP_MAIN_SET_FLAG,               "w" },
   { "clear-flag",       OP_MAIN_CLEAR_FLAG,            "W" },
   { "edit",            OP_EDIT_MESSAGE,                "e" },
+  { "edit-label",      OP_EDIT_LABEL,                  NULL },
   { "edit-type",       OP_EDIT_TYPE,                   "\005" },
   { "forward-message", OP_FORWARD_MESSAGE,             "f" },
   { "flag-message",    OP_FLAG_MESSAGE,                "F" },
index 95a6869baa03b4a01336947a34a64df1cb773807..e9fc058f6c8d015b9c6c960ec7f3ac18af5ff52d 100644 (file)
--- a/globals.h
+++ b/globals.h
@@ -66,6 +66,7 @@ WHERE char *ImapUser INITVAL (NULL);
 #endif
 WHERE char *Inbox;
 WHERE char *Ispell;
+WHERE char *KeywordsSave;
 WHERE char *Locale;
 WHERE char *MailcapPath;
 WHERE char *Maildir;
@@ -153,6 +154,7 @@ WHERE char *TSIconFormat;
 WHERE short TSSupported;
 WHERE char *Username;
 WHERE char *Visual;
+WHERE char *XlabelDelim;
 
 WHERE char *CurrentFolder;
 WHERE char *LastFolder;
@@ -161,6 +163,7 @@ WHERE char *LastFolder;
 WHERE const char *ReleaseDate;
 
 WHERE HASH *Groups;
+WHERE HASH *Labels;
 WHERE HASH *ReverseAlias;
 
 WHERE LIST *AutoViewList INITVAL(0);
diff --git a/hash.c b/hash.c
index 08f717179b91d1dea2e280d2fd0e03378acc5e26..b20e5c50b2ae5f650e58a049af5ee7d0eb81478e 100644 (file)
--- a/hash.c
+++ b/hash.c
@@ -176,3 +176,30 @@ void hash_destroy (HASH **ptr, void (*destroy) (void *))
   FREE (&pptr->table);
   FREE (ptr);          /* __FREE_CHECKED__ */
 }
+
+struct hash_elem *hash_walk(const HASH *table, struct hash_walk_state *state)
+{
+  if (state->last && state->last->next)
+  {
+    state->last = state->last->next;
+    return state->last;
+  }
+
+  if (state->last)
+    state->index++;
+
+  while (state->index < table->nelem)
+  {
+    if (table->table[state->index])
+    {
+      state->last = table->table[state->index];
+      return state->last;
+    }
+    state->index++;
+  } 
+
+  state->index = 0;
+  state->last = NULL;
+  return NULL;
+}
+
diff --git a/hash.h b/hash.h
index fb77d0c14c5cc5d81668612f2ea44e8a2811e45a..2939beccd14ecd07455b2574e54c132fcdae37b2 100644 (file)
--- a/hash.h
+++ b/hash.h
@@ -46,4 +46,11 @@ void hash_delete_hash (HASH * table, int hash, const char *key, const void *data
                       void (*destroy) (void *));
 void hash_destroy (HASH ** hash, void (*destroy) (void *));
 
+struct hash_walk_state {
+  int index;
+  struct hash_elem *last;
+};
+
+struct hash_elem *hash_walk(const HASH *table, struct hash_walk_state *state);
+
 #endif
index fb6a5619180f12c959d4f409e671a6fbd9bddfc5..6ec5cc16717e4138d113c55c8efcee021bc0b58b 100644 (file)
--- a/hcache.c
+++ b/hcache.c
@@ -439,13 +439,13 @@ dump_envelope(ENVELOPE * e, unsigned char *d, int *off, int convert)
   d = dump_char(e->message_id, d, off, 0);
   d = dump_char(e->supersedes, d, off, 0);
   d = dump_char(e->date, d, off, 0);
-  d = dump_char(e->x_label, d, off, convert);
 
   d = dump_buffer(e->spam, d, off, convert);
 
   d = dump_list(e->references, d, off, 0);
   d = dump_list(e->in_reply_to, d, off, 0);
   d = dump_list(e->userhdrs, d, off, convert);
+  d = dump_list(e->labels, d, off, convert);
 
   return d;
 }
@@ -476,13 +476,13 @@ restore_envelope(ENVELOPE * e, const unsigned char *d, int *off, int convert)
   restore_char(&e->message_id, d, off, 0);
   restore_char(&e->supersedes, d, off, 0);
   restore_char(&e->date, d, off, 0);
-  restore_char(&e->x_label, d, off, convert);
 
   restore_buffer(&e->spam, d, off, convert);
 
   restore_list(&e->references, d, off, 0);
   restore_list(&e->in_reply_to, d, off, 0);
   restore_list(&e->userhdrs, d, off, convert);
+  restore_list(&e->labels, d, off, convert);
 }
 
 static int
index eef19e6c552cc2c6fad82e09e6e8b7d59ac8fa19..cab7f6118138a7ff7601969da6daacd1db6f87a4 100644 (file)
--- a/hdrline.c
+++ b/hdrline.c
@@ -695,38 +695,54 @@ hdr_format_str (char *dest,
 
      case 'y':
        if (optional)
-        optional = hdr->env->x_label ? 1 : 0;
+        optional = hdr->env->labels ? 1 : 0;
 
-       mutt_format_s (dest, destlen, prefix, NONULL (hdr->env->x_label));
+       mutt_format_s (dest, destlen, prefix,
+                      mutt_labels(NULL, 0, hdr->env, NULL));
        break;
  
     case 'Y':
-      if (hdr->env->x_label)
+      if (hdr->env->labels == NULL)
       {
-       i = 1;  /* reduce reuse recycle */
-       htmp = NULL;
-       if (flags & MUTT_FORMAT_TREE
-           && (hdr->thread->prev && hdr->thread->prev->message
-               && hdr->thread->prev->message->env->x_label))
-         htmp = hdr->thread->prev->message;
-       else if (flags & MUTT_FORMAT_TREE
-                && (hdr->thread->parent && hdr->thread->parent->message
-                    && hdr->thread->parent->message->env->x_label))
-         htmp = hdr->thread->parent->message;
-       if (htmp && mutt_strcasecmp (hdr->env->x_label,
-                                    htmp->env->x_label) == 0)
-         i = 0;
+        if (optional)
+          optional = 0;
+        mutt_format_s(dest, destlen, prefix, "");
+        break;
       }
       else
-       i = 0;
-
-      if (optional)
-       optional = i;
-
-      if (i)
-        mutt_format_s (dest, destlen, prefix, NONULL (hdr->env->x_label));
-      else
-        mutt_format_s (dest, destlen, prefix, "");
+      {
+        char labels[HUGE_STRING];
+        char labelstmp[HUGE_STRING];
+
+        i = 1;  /* reduce reuse recycle */
+        htmp = NULL;
+        if ((flags & MUTT_FORMAT_TREE) &&
+            hdr->thread->prev &&
+            hdr->thread->prev->message &&
+            hdr->thread->prev->message->env->labels)
+          htmp = hdr->thread->prev->message;
+        else if ((flags & MUTT_FORMAT_TREE) &&
+                 hdr->thread->parent &&
+                 hdr->thread->parent->message &&
+                 hdr->thread->parent->message->env->labels)
+          htmp = hdr->thread->parent->message;
+
+        mutt_labels(labels, sizeof(labels), hdr->env, NULL);
+        if (htmp)
+        {
+          mutt_labels(labelstmp, sizeof(labelstmp), htmp->env, NULL);
+          if (htmp && mutt_strcasecmp (labels, labelstmp) == 0)
+            i = 0;
+        }
+
+        if (optional)
+         optional = i;
+
+        if (i)
+          mutt_format_s (dest, destlen, prefix, labels);
+        else
+          mutt_format_s (dest, destlen, prefix, "");
+      }
 
       break;
 
index 0a759988f30fdd5f6c648f146a142457c250df84..2bdc5d07516e43d1322e4fe17ad341f1028ad2be 100644 (file)
--- a/headers.c
+++ b/headers.c
@@ -211,3 +211,197 @@ void mutt_edit_headers (const char *editor,
     }
   }
 }
+
+void mutt_label_ref_dec(ENVELOPE *env)
+{
+  uintptr_t count;
+  LIST *label;
+
+  if (!env || !env->labels || !Labels)
+    return;
+
+  for (label = env->labels; label; label = label->next)
+  {
+    if (label->data == NULL)
+      continue;
+    count = (uintptr_t)hash_find(Labels, label->data);
+    if (count)
+    {
+      hash_delete(Labels, label->data, NULL, NULL);
+      count--;
+      if (count > 0)
+        hash_insert(Labels, label->data, (void *)count, 0);
+    }
+    dprint(1, (debugfile, "--label %s: %d\n", label->data, count));
+  }
+}
+
+void mutt_label_ref_inc(ENVELOPE *env)
+{
+  uintptr_t count;
+  LIST *label;
+
+  if (!env || !env->labels || !Labels)
+    return;
+
+  for (label = env->labels; label; label = label->next)
+  {
+    if (label->data == NULL)
+      continue;
+    count = (uintptr_t)hash_find(Labels, label->data);
+    if (count)
+      hash_delete(Labels, label->data, NULL, NULL);
+    count++;  /* was zero if not found */
+    hash_insert(Labels, label->data, (void *)count, 0);
+    dprint(1, (debugfile, "++label %s: %d\n", label->data, count));
+  }
+}
+
+/*
+ * set labels on a message
+ */
+static int label_message(HEADER *hdr, char *new)
+{
+  if (hdr == NULL)
+    return 0;
+  if (hdr->env->labels == NULL && new == NULL)
+    return 0;
+  if (hdr->env->labels != NULL && new != NULL)
+  {
+    char old[HUGE_STRING];
+    mutt_labels(old, sizeof(old), hdr->env, NULL);
+    if (!strcmp(old, new))
+      return 0;
+  }
+
+  if (hdr->env->labels != NULL)
+  {
+    mutt_label_ref_dec(hdr->env);
+    mutt_free_list(&hdr->env->labels);
+  }
+
+  if (new == NULL)
+    hdr->env->labels = NULL;
+  else
+  {
+    char *last, *label;
+
+    for (label = strtok_r(new, ",", &last); label;
+         label = strtok_r(NULL, ",", &last)) 
+    {
+      SKIPWS(label);
+      if (mutt_find_list(hdr->env->labels, label))
+        continue;
+      if (hdr->env->labels == NULL)
+      {
+        hdr->env->labels = mutt_new_list();
+        hdr->env->labels->data = safe_strdup(label);
+      }
+      else
+        mutt_add_list(hdr->env->labels, label);
+    }
+    mutt_label_ref_inc(hdr->env);
+  }
+  return hdr->changed = hdr->label_changed = 1;
+}
+
+int mutt_label_message(HEADER *hdr)
+{
+  char buf[LONG_STRING], *new;
+  int i;
+  int changed;
+
+  *buf = '\0';
+  if (hdr != NULL && hdr->env->labels != NULL)
+    mutt_labels(buf, sizeof(buf)-2, hdr->env, NULL);
+
+  /* add a comma-space so that new typing is a new keyword */
+  if (buf[0])
+    strcat(buf, ", ");    /* __STRCAT_CHECKED__ */
+
+  if (mutt_get_field("Label: ", buf, sizeof(buf), MUTT_LABEL /* | MUTT_CLEAR */) != 0)
+    return 0;
+
+  new = buf;
+  SKIPWS(new);
+  if (new && *new)
+  {
+    char *p;
+    int len = strlen(new);
+    p = &new[len]; /* '\0' */
+    while (p > new)
+    {
+      if (!isspace((unsigned char)*(p-1)) && *(p-1) != ',')
+        break;
+      p--;
+    }
+    *p = '\0';
+  }
+  if (*new == '\0')
+    new = NULL;
+
+  changed = 0;
+  if (hdr != NULL) {
+    changed += label_message(hdr, new);
+  } else {
+#define HDR_OF(index) Context->hdrs[Context->v2r[(index)]]
+    for (i = 0; i < Context->vcount; ++i) {
+      if (HDR_OF(i)->tagged)
+        if (label_message(HDR_OF(i), new)) {
+          ++changed;
+        }
+    }
+  }
+
+  return changed;
+}
+
+/* scan a context (mailbox) and hash all labels we find */
+void mutt_scan_labels(CONTEXT *ctx)
+{
+  int i;
+
+  if (!ctx)
+    return;
+
+  for (i = 0; i < ctx->msgcount; i++)
+    if (ctx->hdrs[i]->env->labels)
+      mutt_label_ref_inc(ctx->hdrs[i]->env);
+}
+
+
+char *mutt_labels(char *dst, int sz, ENVELOPE *env, char *sep)
+{
+  static char sbuf[HUGE_STRING];
+  int off = 0;
+  int len;
+  LIST *label;
+
+  if (sep == NULL)
+    sep = ", ";
+
+  if (dst == NULL)
+  {
+    dst = sbuf;
+    sz = sizeof(sbuf);
+  }
+
+  *dst = '\0';
+
+  for (label = env->labels; label; label = label->next)
+  {
+    if (label->data == NULL)
+      continue;
+    len = MIN(mutt_strlen(label->data), sz-off);
+    strfcpy(&dst[off], label->data, len+1);
+    off += len;
+    if (label->next)
+    {
+      len = MIN(mutt_strlen(sep), sz-off);
+      strfcpy(&dst[off], sep, len+1);
+      off += len;
+    }
+  }
+
+  return dst;
+}
index 2e3d27d3d40a4e966fb3631e47df69010fec6769..fab1d03c20e3b076c53a2d23b73135923df60b80 100644 (file)
@@ -1240,7 +1240,7 @@ int imap_sync_mailbox (CONTEXT* ctx, int expunge, int* index_hint)
        * we delete the message and reupload it.
        * This works better if we're expunging, of course. */
       if ((h->env && (h->env->refs_changed || h->env->irt_changed)) ||
-         h->attach_del)
+         h->attach_del || h->label_changed)
       {
         mutt_message (_("Saving changed messages... [%d/%d]"), n+1,
                       ctx->msgcount);
@@ -1250,6 +1250,7 @@ int imap_sync_mailbox (CONTEXT* ctx, int expunge, int* index_hint)
          dprint (1, (debugfile, "imap_sync_mailbox: Error opening mailbox in append mode\n"));
        else
          _mutt_save_message (h, appendctx, 1, 0, 0);
+       h->label_changed = 0;
       }
     }
   }
index 5ce585868f716b9e987aaa79c29182f372f6cff1..8211d3a1147b9d2980e15da69e17a68b9ca3940e 100644 (file)
@@ -69,7 +69,7 @@ int imap_read_headers (IMAP_DATA* idata, int msgbegin, int msgend)
   int rc, mfhrc, oldmsgcount;
   int fetchlast = 0;
   int maxuid = 0;
-  static const char * const want_headers = "DATE FROM SUBJECT TO CC MESSAGE-ID REFERENCES CONTENT-TYPE CONTENT-DESCRIPTION IN-REPLY-TO REPLY-TO LINES LIST-POST X-LABEL";
+  static const char * const want_headers = "DATE FROM SUBJECT TO CC MESSAGE-ID REFERENCES CONTENT-TYPE CONTENT-DESCRIPTION IN-REPLY-TO REPLY-TO LINES LIST-POST X-LABEL X-KEYWORDS X-MOZILLA-KEYS KEYWORDS";
   progress_t progress;
   int retval = -1;
 
@@ -407,6 +407,7 @@ int imap_fetch_message (CONTEXT *ctx, MESSAGE *msg, int msgno)
   IMAP_CACHE *cache;
   int read;
   int rc;
+
   /* Sam's weird courier server returns an OK response even when FETCH
    * fails. Thanks Sam. */
   short fetched = 0;
diff --git a/init.c b/init.c
index 0586638dea29d60f0f117cb014a2fd3445610270..2c4cad1d1824bb6b8da7d5c43f13ff0d6297f58f 100644 (file)
--- a/init.c
+++ b/init.c
@@ -3297,3 +3297,58 @@ static const char* myvar_get (const char* var)
 
   return NULL;
 }
+
+int mutt_label_complete (char *buffer, size_t len, int pos, int numtabs)
+{
+  char *pt = buffer;
+  int spaces; /* keep track of the number of leading spaces on the line */
+  int prefix;
+
+  SKIPWS (buffer);
+  spaces = buffer - pt;
+
+  for (pt = buffer; pt && *pt && *(pt+1); pt++);
+  for (; pt > buffer && !isspace(*(pt-1)); pt--);
+  prefix = pt - buffer;
+
+  /* first TAB. Collect all the matches */
+  if (numtabs == 1)
+  {
+    struct hash_elem *entry;
+    struct hash_walk_state state;
+
+    Num_matched = 0;
+    strfcpy (User_typed, pt, sizeof (User_typed));
+    memset (Matches, 0, Matches_listsize);
+    memset (Completed, 0, sizeof (Completed));
+    memset (&state, 0, sizeof(state));
+    while ((entry = hash_walk(Labels, &state)))
+      candidate (Completed, User_typed, entry->key, sizeof (Completed));
+    matches_ensure_morespace (Num_matched);
+    qsort(Matches, Num_matched, sizeof(char *), (sort_t *) mutt_strcasecmp);
+    Matches[Num_matched++] = User_typed;
+
+    /* All matches are stored. Longest non-ambiguous string is ""
+     * i.e. dont change 'buffer'. Fake successful return this time */
+    if (User_typed[0] == 0)
+      return 1;
+  }
+
+  if (Completed[0] == 0 && User_typed[0])
+    return 0;
+
+   /* Num_matched will _always_ be atleast 1 since the initial
+    * user-typed string is always stored */
+  if (numtabs == 1 && Num_matched == 2)
+    snprintf(Completed, sizeof(Completed), "%s", Matches[0]);
+  else if (numtabs > 1 && Num_matched > 2)
+    /* cycle thru all the matches */
+    snprintf(Completed, sizeof(Completed), "%s", 
+             Matches[(numtabs - 2) % Num_matched]);
+
+  /* return the completed label */
+  strncpy (&buffer[prefix], Completed, len - spaces);
+
+  return 1;
+}
+
diff --git a/init.h b/init.h
index 3f3f96d9dd57e3d12e609bd9e3c01f8cc3fd7d93..2ce9148cc8f0a7f1b8a832657e8e27fd7b1985aa 100644 (file)
--- a/init.h
+++ b/init.h
@@ -1395,6 +1395,28 @@ struct option_t MuttVars[] = {
   ** from your spool mailbox to your $$mbox mailbox, or as a result of
   ** a ``$mbox-hook'' command.
   */
+  { "keywords_legacy", DT_BOOL, R_NONE, OPTKEYWORDSLEGACY, 1 },
+  /*
+  ** .pp
+  ** If \fIset\fP, keywords/labels/tags will be written to whatever
+  ** legacy, nonstandard headers (X-Label, X-Keywords, X-Mozilla-Keys)
+  ** they were sourced from.
+  ** .pp
+  ** If both ``$$keywords_legacy'' and
+  ** ``$$keywords_standard'' are \fCfalse\fP, mutt will save keywords
+  ** to legacy headers to ensure that it does not lose your labels.
+  */
+  { "keywords_standard", DT_BOOL, R_NONE, OPTKEYWORDSSTANDARD, 0 },
+  /*
+  ** .pp
+  ** If \fIset\fP, keywords/labels/tags will be written to the
+  ** RFC2822-standard Keywords: header; this may imply a conversion from
+  ** legacy headers.
+  ** .pp
+  ** If both ``$$keywords_legacy'' and
+  ** ``$$keywords_standard'' are \fCfalse\fP, mutt will save keywords
+  ** to legacy headers to ensure that it does not lose your labels.
+  */
   { "locale",          DT_STR,  R_BOTH, UL &Locale, UL "C" },
   /*
   ** .pp
@@ -3763,6 +3785,20 @@ struct option_t MuttVars[] = {
   {"xterm_set_titles", DT_SYN,  R_NONE, UL "ts_enabled", 0 },
   /*
   */
+  { "xlabel_delimiter", DT_STR, R_NONE, UL &XlabelDelim, UL "" },
+  /*
+  ** .pp
+  ** The character used to delimit distinct keywords in X-Label headers.
+  ** X-Label is primarily a Mutt artifact, and the semantics of the field
+  ** were never defined: it is free-form text.  However interaction with
+  ** X-Keywords:, X-Mozilla-Keys:, and Keywords: requires that we adopt
+  ** some means of identifying separate keywords within the field.  Set
+  ** this to your personal convention.
+  ** .pp
+  ** This affect both parsing existing X-Label headers and writing new
+  ** X-Label headers.  You can modify this variable in runtime to accomplish
+  ** various kinds of conversion.
+  */
   /*--*/
   { NULL, 0, 0, 0, 0 }
 };
@@ -3779,6 +3815,7 @@ const struct mapping_t SortMethods[] = {
   { "to",              SORT_TO },
   { "score",           SORT_SCORE },
   { "spam",            SORT_SPAM },
+  { "label",           SORT_LABEL },
   { NULL,               0 }
 };
 
@@ -3798,6 +3835,7 @@ const struct mapping_t SortAuxMethods[] = {
   { "to",              SORT_TO },
   { "score",           SORT_SCORE },
   { "spam",            SORT_SPAM },
+  { "label",           SORT_LABEL },
   { NULL,               0 }
 };
 
diff --git a/main.c b/main.c
index b6541697f4c9a7a3c14d6862393197798bd82318..a1789a1184341a4dc104fbb3a96214e463794586 100644 (file)
--- a/main.c
+++ b/main.c
@@ -1225,9 +1225,13 @@ int main (int argc, char **argv)
 #ifdef USE_SIDEBAR
       mutt_sb_set_open_buffy ();
 #endif
+      Labels = hash_create (131, 0);
+      mutt_scan_labels(Context);
       mutt_index_menu ();
       if (Context)
        FREE (&Context);
+      if (Labels)
+        hash_destroy(&Labels, NULL);
     }
 #ifdef USE_IMAP
     imap_logout_all ();
diff --git a/mh.c b/mh.c
index 371656af4c6b1502b433d2ff1a1e88aa8108c726..71857c87f8b8d0e6f7024811d67c7e643996ac98 100644 (file)
--- a/mh.c
+++ b/mh.c
@@ -1790,7 +1790,7 @@ static int mh_sync_message (CONTEXT * ctx, int msgno)
 {
   HEADER *h = ctx->hdrs[msgno];
 
-  if (h->attach_del || 
+  if (h->attach_del || h->label_changed ||
       (h->env && (h->env->refs_changed || h->env->irt_changed)))
     if (mh_rewrite_message (ctx, msgno) != 0)
       return -1;
@@ -1802,7 +1802,7 @@ static int maildir_sync_message (CONTEXT * ctx, int msgno)
 {
   HEADER *h = ctx->hdrs[msgno];
 
-  if (h->attach_del || 
+  if (h->attach_del || h->label_changed ||
       (h->env && (h->env->refs_changed || h->env->irt_changed)))
   {
     /* when doing attachment deletion/rethreading, fall back to the MH case. */
@@ -1924,6 +1924,7 @@ int mh_sync_mailbox (CONTEXT * ctx, int *index_hint)
       }
     }
     else if (ctx->hdrs[i]->changed || ctx->hdrs[i]->attach_del ||
+            ctx->hdrs[i]->label_changed ||
             (ctx->magic == MUTT_MAILDIR
              && (option (OPTMAILDIRTRASH) || ctx->hdrs[i]->trash)
              && (ctx->hdrs[i]->deleted != ctx->hdrs[i]->trash)))
diff --git a/mutt.h b/mutt.h
index 2cf55b102640049fc25711e381550be4677208ad..007fc2e75b80e5e1a8c83074cb35e060fc985697 100644 (file)
--- a/mutt.h
+++ b/mutt.h
@@ -88,6 +88,7 @@
 #define  MUTT_CLEAR   (1<<5) /* clear input if printable character is pressed */
 #define  MUTT_COMMAND (1<<6) /* do command completion */
 #define  MUTT_PATTERN (1<<7) /* pattern mode - only used for history classes */
+#define  MUTT_LABEL   (1<<8) /* do label completion */
 
 /* flags for mutt_get_token() */
 #define MUTT_TOKEN_EQUAL      1       /* treat '=' as a special */
@@ -313,6 +314,12 @@ enum
 #define MUTT_SPAM          1
 #define MUTT_NOSPAM        2
 
+/* flags for keywords headers */
+#define MUTT_X_LABEL         (1<<0)  /* introduced to mutt in 2000 */
+#define MUTT_X_KEYWORDS      (1<<1)  /* used in c-client, dovecot */
+#define MUTT_X_MOZILLA_KEYS  (1<<2)  /* tbird */
+#define MUTT_KEYWORDS        (1<<3)  /* rfc2822 */
+
 /* boolean vars */
 enum
 {
@@ -388,6 +395,8 @@ enum
   OPTIMPLICITAUTOVIEW,
   OPTINCLUDEONLYFIRST,
   OPTKEEPFLAGGED,
+  OPTKEYWORDSLEGACY,
+  OPTKEYWORDSSTANDARD,
   OPTMAILCAPSANITIZE,
   OPTMAILCHECKRECENT,
   OPTMAILCHECKSTATS,
@@ -609,11 +618,12 @@ typedef struct envelope
   char *message_id;
   char *supersedes;
   char *date;
-  char *x_label;
   BUFFER *spam;
   LIST *references;            /* message references (in reverse order) */
   LIST *in_reply_to;           /* in-reply-to header content */
   LIST *userhdrs;              /* user defined headers */
+  LIST *labels;
+  int kwtypes;
 
   unsigned int irt_changed : 1; /* In-Reply-To changed to link/break threads */
   unsigned int refs_changed : 1; /* References changed to break thread */
@@ -747,6 +757,7 @@ typedef struct header
                                         * This flag is used by the maildir_trash
                                         * option.
                                         */
+  unsigned int label_changed : 1;      /* editable - used for syncing */
   
   /* timezone of the sender of this message */
   unsigned int zhours : 5;
index 209c7ab90306e25a09ee8a9b48a9ac9471b59b14..2d81b21f281e7d2f0e0806af6f1a7f3816d910df 100644 (file)
--- a/muttlib.c
+++ b/muttlib.c
@@ -715,13 +715,14 @@ void mutt_free_envelope (ENVELOPE **p)
   FREE (&(*p)->message_id);
   FREE (&(*p)->supersedes);
   FREE (&(*p)->date);
-  FREE (&(*p)->x_label);
 
   mutt_buffer_free (&(*p)->spam);
 
   mutt_free_list (&(*p)->references);
   mutt_free_list (&(*p)->in_reply_to);
   mutt_free_list (&(*p)->userhdrs);
+  mutt_label_ref_dec ((*p));
+  mutt_free_list (&(*p)->labels);
   FREE (p);            /* __FREE_CHECKED__ */
 }
 
@@ -744,7 +745,7 @@ void mutt_merge_envelopes(ENVELOPE* base, ENVELOPE** extra)
   MOVE_ELEM(message_id);
   MOVE_ELEM(supersedes);
   MOVE_ELEM(date);
-  MOVE_ELEM(x_label);
+  MOVE_ELEM(labels);
   if (!base->refs_changed)
   {
     MOVE_ELEM(references);
diff --git a/pager.c b/pager.c
index d7674eb30cb92cc31cfcf59ca39096e1f7624378..54c8758d3c9551c3dd5dabb0a05579ab9faa8f80 100644 (file)
--- a/pager.c
+++ b/pager.c
@@ -2807,6 +2807,18 @@ search_next:
        redraw = REDRAW_FULL;
        break;
 
+     case OP_EDIT_LABEL:
+        CHECK_MODE(IsHeader (extra));
+        rc = mutt_label_message(extra->hdr);
+        if (rc > 0) {
+          Context->changed = 1;
+          redraw = REDRAW_FULL;
+          mutt_message ("%d label%s changed.", rc, rc == 1 ? "" : "s");
+        }
+        else {
+          mutt_message _("No labels changed.");
+        }
+        break;
 
       case OP_MAIL_KEY:
         if (!(WithCrypto & APPLICATION_PGP))
diff --git a/parse.c b/parse.c
index 92cfeb0ffa25ce8433fb15ac91f32daef92ee0f7..e11977e2261be8466931e389ffeb6af3bb8b6a60 100644 (file)
--- a/parse.c
+++ b/parse.c
@@ -981,6 +981,7 @@ int mutt_parse_rfc822_line (ENVELOPE *e, HEADER *hdr, char *line, char *p, short
 {
   int matched = 0;
   LIST *last = NULL;
+  int kwtype = 0;
   
   if (lastp)
     last = *lastp;
@@ -1087,7 +1088,14 @@ int mutt_parse_rfc822_line (ENVELOPE *e, HEADER *hdr, char *line, char *p, short
       matched = 1;
     }
     break;
-    
+
+    case 'k':
+    if (!ascii_strcasecmp (line+1, "eywords"))
+    {
+      kwtype = MUTT_KEYWORDS;
+    }
+    break;
+
     case 'l':
     if (!ascii_strcasecmp (line + 1, "ines"))
     {
@@ -1267,15 +1275,21 @@ int mutt_parse_rfc822_line (ENVELOPE *e, HEADER *hdr, char *line, char *p, short
     }
     else if (ascii_strcasecmp (line+1, "-label") == 0)
     {
-      FREE(&e->x_label);
-      e->x_label = safe_strdup(p);
-      matched = 1;
+      kwtype = MUTT_X_LABEL;
     }
-    
+    else if (!ascii_strcasecmp (line+1, "-keywords"))
+    {
+      kwtype = MUTT_X_KEYWORDS;
+    }
+    else if (!ascii_strcasecmp (line+1, "-mozilla-keys"))
+    {
+      kwtype = MUTT_X_MOZILLA_KEYS;
+    }
+
     default:
     break;
   }
-  
+
   /* Keep track of the user-defined headers */
   if (!matched && user_hdrs)
   {
@@ -1298,12 +1312,59 @@ int mutt_parse_rfc822_line (ENVELOPE *e, HEADER *hdr, char *line, char *p, short
       rfc2047_decode (&last->data);
   }
 
+  if (kwtype)
+  {
+    char *last, *label;
+    char *text = safe_strdup(p);
+    char *sep;
+
+    if (kwtype == MUTT_KEYWORDS)
+      sep = ",";
+    else if (kwtype == MUTT_X_LABEL)
+      sep = XlabelDelim;
+    else
+      sep = " ";
+
+    rfc2047_decode(&text);
+    if (sep == NULL || *sep == '\0')
+    {
+      SKIPWS(text);
+      if (!mutt_find_list(e->labels, text))
+      {
+        if (e->labels)
+          mutt_add_list(e->labels, text);
+        else
+        {
+          e->labels = mutt_new_list();
+          e->labels->data = safe_strdup(text);
+        }
+      }
+    }
+    else for (label = strtok_r(text, sep, &last); label;
+              label = strtok_r(NULL, sep, &last))
+    {
+      SKIPWS(label);
+      if (mutt_find_list(e->labels, label))
+        continue;
+      if (e->labels)
+        mutt_add_list(e->labels, label);
+      else
+      {
+        e->labels = mutt_new_list();
+        e->labels->data = safe_strdup(label);
+      }
+    }
+    e->kwtypes |= kwtype;
+    kwtype = 0;
+    matched = 1;
+  }
+
   done:
   
   *lastp = last;
   return matched;
 }
-  
+
   
 /* mutt_read_rfc822_header() -- parses a RFC822 header
  *
@@ -1441,7 +1502,6 @@ ENVELOPE *mutt_read_rfc822_header (FILE *f, HEADER *hdr, short user_hdrs,
     rfc2047_decode_adrlist (e->mail_followup_to);
     rfc2047_decode_adrlist (e->return_path);
     rfc2047_decode_adrlist (e->sender);
-    rfc2047_decode (&e->x_label);
 
     if (e->subject)
     {
index 85d38b5f44f57053c295becb6d9aead903a38c0e..0d40f453a82b07dc896c8752520dac5e6485a48d 100644 (file)
--- a/pattern.c
+++ b/pattern.c
@@ -1209,7 +1209,19 @@ mutt_pattern_exec (struct pattern_t *pat, pattern_exec_flag flags, CONTEXT *ctx,
        break;
      return (pat->not ^ ((h->security & APPLICATION_PGP) && (h->security & PGPKEY)));
     case MUTT_XLABEL:
-      return (pat->not ^ (h->env->x_label && patmatch (pat, h->env->x_label) == 0));
+      {
+        LIST *label;
+        int result = 0;
+        for (label = h->env->labels; label; label = label->next)
+        {
+          if (label->data == NULL)
+            continue;
+          result = patmatch (pat, label->data) == 0;
+          if (result)
+            break;
+        }
+        return pat->not ^ result;
+      }
     case MUTT_HORMEL:
       return (pat->not ^ (h->env->spam && h->env->spam->data && patmatch (pat, h->env->spam->data) == 0));
     case MUTT_DUPLICATED:
index 19d640032ae1da433f17215d5b46439a66c7781d..0d6d9e8c51f6c82b3525d5702d4116493433384b 100644 (file)
--- a/protos.h
+++ b/protos.h
@@ -186,6 +186,12 @@ void mutt_edit_content_type (HEADER *, BODY *, FILE *);
 void mutt_edit_file (const char *, const char *);
 void mutt_edit_headers (const char *, const char *, HEADER *, char *, size_t);
 int mutt_filter_unprintable (char **);
+void mutt_label_ref_dec(ENVELOPE *);
+void mutt_label_ref_inc(ENVELOPE *);
+int mutt_label_message (HEADER *);
+void mutt_scan_labels (CONTEXT *);
+int mutt_label_complete (char *, size_t, int, int);
+char *mutt_labels(char *, int, ENVELOPE *, char *);
 void mutt_curses_error (const char *, ...);
 void mutt_curses_message (const char *, ...);
 void mutt_encode_descriptions (BODY *, short);
index 771eae2d6287488f5b26a5f26faef41325215988..64dfa1b186bc8f7473d4127488e56ecb50118056 100644 (file)
--- a/sendlib.c
+++ b/sendlib.c
@@ -2492,7 +2492,6 @@ void mutt_prepare_envelope (ENVELOPE *env, int final)
   rfc2047_encode_adrlist (env->from, "From");
   rfc2047_encode_adrlist (env->mail_followup_to, "Mail-Followup-To");
   rfc2047_encode_adrlist (env->reply_to, "Reply-To");
-  rfc2047_encode_string (&env->x_label);
 
   if (env->subject)
   {
@@ -2517,7 +2516,6 @@ void mutt_unprepare_envelope (ENVELOPE *env)
   rfc2047_decode_adrlist (env->from);
   rfc2047_decode_adrlist (env->reply_to);
   rfc2047_decode (&env->subject);
-  rfc2047_decode (&env->x_label);
 }
 
 static int _mutt_bounce_message (FILE *fp, HEADER *h, ADDRESS *to, const char *resent_from,
diff --git a/sort.c b/sort.c
index 76e9e797230a8200e03fc4435bd7260626f36e9e..4136545e070376f41001b0301899f9bfe47a9dd0 100644 (file)
--- a/sort.c
+++ b/sort.c
@@ -210,6 +210,46 @@ static int compare_spam (const void *a, const void *b)
   return (SORTCODE(result));
 }
 
+int compare_label (const void *a, const void *b)
+{
+  HEADER **ppa = (HEADER **) a;
+  HEADER **ppb = (HEADER **) b;
+  int     ahas, bhas, result = 0;
+  LIST *la, *lb;
+
+  /* As with compare_spam, not all messages will have the x-label
+   * property.  Blank X-Labels are treated as null in the index
+   * display, so we'll consider them as null for sort, too.       */
+  ahas = (*ppa)->env && (*ppa)->env->labels;
+  bhas = (*ppb)->env && (*ppb)->env->labels;
+
+  /* First we bias toward a message with a label, if the other does not. */
+  if (ahas && !bhas)
+    return (SORTCODE(-1));
+  if (!ahas && bhas)
+    return (SORTCODE(1));
+
+  /* If neither has a label, use aux sort. */
+  if (!ahas && !bhas)
+  {
+    AUXSORT(result, a, b);
+    return (SORTCODE(result));
+  }
+
+  /* If both have a label, we just do a lexical compare. */
+  for (la = (*ppa)->env->labels, lb = (*ppb)->env->labels;
+       la && la->data && lb && lb->data && result == 0;
+       la = la->next, lb = lb->next)
+  {
+    result = mutt_strcasecmp(la->data, lb->data);
+  }
+  if (result == 0 && la == NULL)
+    return (SORTCODE(-1));
+  if (result == 0 && lb == NULL)
+    return (SORTCODE(1));
+  return (SORTCODE(result));
+}
+
 sort_t *mutt_get_sort_func (int method)
 {
   switch (method & SORT_MASK)
@@ -232,6 +272,8 @@ sort_t *mutt_get_sort_func (int method)
       return (compare_score);
     case SORT_SPAM:
       return (compare_spam);
+    case SORT_LABEL:
+      return (compare_label);
     default:
       return (NULL);
   }
diff --git a/sort.h b/sort.h
index 26afdc6285ab9dda96090d81de248ed4afed9982..372783589a670225d19dead047234145b82703d7 100644 (file)
--- a/sort.h
+++ b/sort.h
@@ -35,6 +35,7 @@
 #define SORT_COUNT_NEW 16
 #define SORT_FLAGGED   17
 #define SORT_PATH      18
+#define SORT_LABEL     19
 
 /* dgc: Sort & SortAux are shorts, so I'm bumping these bitflags up from
  * bits 4 & 5 to bits 8 & 9 to make room for more sort keys in the future. */