<<< Date Index >>>     <<< Thread Index >>>

[PATCH 4 of 5] Unify label/keyword handling



# HG changeset patch
# User David Champion <dgc@xxxxxxxxxxxx>
# Date 1294082431 21600
# Branch HEAD
# Node ID 32fa2504d528f3d84863712cb0874272e9f2c63d
# Parent  cc2da8afdcb7db1a334d1d724bf27a7520877476
Unify label/keyword handling.

Since x-labels were added to mutt in 2000, a number of other approaches
to what we now call 'tagging' have also emerged.  One of them was even
made standard in RFC 2822.  This update unifies the handling of all these
strategies.

We start by changing mutt's internal keyword storage from a single
string which may contain whitespace to a list of discrete keywords.
This has advantages for keyword completion as well as for portabilty
among varying "standards" for keyword storage.  This may represent
a significant change for existing mutt users who have set x-labels
containing spaces, and should be regarded with suspicion.  The
advantages are significant, though.

Next we allow mutt to parse keywords into this internal list from
any of the following headers: X-Label (freeform), X-Keywords
(space-delimited), X-Mozilla-Keys (space-delimited), and Keywords (RFC
2822, comma-space-delimited).  Mutt remembers which headers it sourced
keywords from, and will rewrite those headers when saving messages for
compatibility with the mailer of origin.

(X-Label was specified as freeform text by mutt, its only known
implementation.  X-Labels have been used both as a 'tagging' device,
probably with space delimiting, and as a 'memo' field, where
space-delimited parsing would ruin the semantics of the memo.  By
default mutt will not split X-Labels at all.  Set $xlabel_delimiter if
your needs vary.)

Finally we add the save_keywords boolean, which defaults to FALSE.  When
true, this boolean causes mutt to always save all keywords to the
Keywords: header in addition to whatever origin headers were retained
for compatibility.

Overall this represents convergence path for all competing
labelling/tagging/kewording systems toward one that is specified by RFC.

diff --git a/copy.c b/copy.c
--- a/copy.c
+++ b/copy.c
@@ -111,9 +111,14 @@
        ignore = 0;
       }
 
-      if (flags & CH_UPDATE_LABEL &&
-         mutt_strncasecmp ("X-Label:", buf, 8) == 0)
-       continue;
+      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);
@@ -416,13 +421,58 @@
       fprintf (out, "Lines: %d\n", h->lines);
   }
 
-  if (flags & CH_UPDATE_LABEL && h->xlabel_changed)
+  if (flags & CH_UPDATE_LABEL && h->label_changed)
   {
-    h->xlabel_changed = 0;
-    if (h->env->x_label != NULL)
-      if (fprintf(out, "X-Label: %s\n", h->env->x_label) !=
-                 10 + strlen(h->env->x_label))
+    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 & M_X_LABEL) &&
+          (option(OPTKEYWORDSLEGACY) || option(OPTKEYWORDSSTANDARD) == 0))
+      {
+        mutt_labels(buf, sizeof(buf), h->env, XlabelDelim);
+        tmp = 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 & M_X_KEYWORDS) &&
+          (option(OPTKEYWORDSLEGACY) || option(OPTKEYWORDSSTANDARD) == 0))
+      {
+        mutt_labels(buf, sizeof(buf), h->env, " ");
+        tmp = 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 & M_X_MOZILLA_KEYS) &&
+          (option(OPTKEYWORDSLEGACY) || option(OPTKEYWORDSSTANDARD) == 0))
+      {
+        mutt_labels(buf, sizeof(buf), h->env, " ");
+        tmp = 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 & M_KEYWORDS) ||
+                        option(OPTKEYWORDSSTANDARD)))
+      {
+        mutt_labels(buf, sizeof(buf), h->env, NULL);
+        tmp = 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)
@@ -505,7 +555,7 @@
       _mutt_make_string (prefix, sizeof (prefix), NONULL (Prefix), Context, 
hdr, 0);
   }
 
-  if (hdr->xlabel_changed)
+  if (hdr->label_changed)
     chflags |= CH_UPDATE_LABEL;
 
   if ((flags & M_CM_NOHEADER) == 0)
diff --git a/globals.h b/globals.h
--- a/globals.h
+++ b/globals.h
@@ -66,6 +66,7 @@
 #endif
 WHERE char *Inbox;
 WHERE char *Ispell;
+WHERE char *KeywordsSave;
 WHERE char *Locale;
 WHERE char *MailcapPath;
 WHERE char *Maildir;
@@ -141,6 +142,7 @@
 WHERE char *Tochars;
 WHERE char *Username;
 WHERE char *Visual;
+WHERE char *XlabelDelim;
 
 WHERE char *CurrentFolder;
 WHERE char *LastFolder;
diff --git a/hcache.c b/hcache.c
--- a/hcache.c
+++ b/hcache.c
@@ -439,13 +439,13 @@
   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_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
diff --git a/hdrline.c b/hdrline.c
--- a/hdrline.c
+++ b/hdrline.c
@@ -676,38 +676,54 @@
 
      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 & M_FORMAT_TREE
-           && (hdr->thread->prev && hdr->thread->prev->message
-               && hdr->thread->prev->message->env->x_label))
-         htmp = hdr->thread->prev->message;
-       else if (flags & M_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;
+      {
+        char labels[HUGE_STRING];
+        char labelstmp[HUGE_STRING];
 
-      if (optional)
-       optional = i;
+        i = 1;  /* reduce reuse recycle */
+        htmp = NULL;
+        if ((flags & M_FORMAT_TREE) &&
+            hdr->thread->prev &&
+            hdr->thread->prev->message &&
+            hdr->thread->prev->message->env->labels)
+          htmp = hdr->thread->prev->message;
+        else if ((flags & M_FORMAT_TREE) &&
+                 hdr->thread->parent &&
+                 hdr->thread->parent->message &&
+                 hdr->thread->parent->message->env->labels)
+          htmp = hdr->thread->parent->message;
 
-      if (i)
-        mutt_format_s (dest, destlen, prefix, NONULL (hdr->env->x_label));
-      else
-        mutt_format_s (dest, destlen, prefix, "");
+        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;
 
diff --git a/headers.c b/headers.c
--- a/headers.c
+++ b/headers.c
@@ -216,58 +216,91 @@
   }
 }
 
-static void label_ref_dec(char *label)
+void mutt_label_ref_dec(ENVELOPE *env)
 {
   uintptr_t count;
+  LIST *label;
 
-  count = (uintptr_t)hash_find(Labels, label);
-  if (count)
+  for (label = env->labels; label; label = label->next)
   {
-    hash_delete(Labels, label, NULL, NULL);
-    count--;
-    if (count > 0)
-      hash_insert(Labels, label, (void *)count, 0);
+    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));
   }
 }
 
-static void label_ref_inc(char *label)
+void mutt_label_ref_inc(ENVELOPE *env)
 {
   uintptr_t count;
+  LIST *label;
 
-  count = (uintptr_t)hash_find(Labels, label);
-  if (count)
-    hash_delete(Labels, label, NULL, NULL);
-  count++;  /* was zero if not found */
-  hash_insert(Labels, label, (void *)count, 0);
+  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));
+  }
 }
 
 /*
- * add an X-Label: field.
+ * set labels on a message
  */
 static int label_message(HEADER *hdr, char *new)
 {
   if (hdr == NULL)
     return 0;
-  if (hdr->env->x_label == NULL && new == NULL)
+  if (hdr->env->labels == NULL && new == NULL)
     return 0;
-  if (hdr->env->x_label != NULL && new != NULL &&
-      strcmp(hdr->env->x_label, new) == 0)
-    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->x_label != NULL)
+  if (hdr->env->labels != NULL)
   {
-    label_ref_dec(hdr->env->x_label);
-    FREE(&hdr->env->x_label);
+    mutt_label_ref_dec(hdr->env);
+    mutt_free_list(&hdr->env->labels);
   }
 
   if (new == NULL)
-    hdr->env->x_label = NULL;
+    hdr->env->labels = NULL;
   else
   {
-    hdr->env->x_label = safe_strdup(new);
-    label_ref_inc(hdr->env->x_label);
+    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->xlabel_changed = 1;
+  return hdr->changed = hdr->label_changed = 1;
 }
 
 int mutt_label_message(HEADER *hdr)
@@ -277,15 +310,31 @@
   int changed;
 
   *buf = '\0';
-  if (hdr != NULL && hdr->env->x_label != NULL) {
-    strncpy(buf, hdr->env->x_label, LONG_STRING);
-  }
+  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), M_LABEL /* | M_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;
 
@@ -313,7 +362,43 @@
   int i;
 
   for (i = 0; i < ctx->msgcount; i++)
-    if (ctx->hdrs[i]->env->x_label)
-      label_ref_inc(ctx->hdrs[i]->env->x_label);
+    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;
+}
diff --git a/imap/imap.c b/imap/imap.c
--- a/imap/imap.c
+++ b/imap/imap.c
@@ -1225,7 +1225,7 @@
        * 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->xlabel_changed)
+         h->attach_del || h->label_changed)
       {
         mutt_message (_("Saving changed messages... [%d/%d]"), n+1,
                       ctx->msgcount);
@@ -1235,7 +1235,7 @@
          dprint (1, (debugfile, "imap_sync_mailbox: Error opening mailbox in 
append mode\n"));
        else
          _mutt_save_message (h, appendctx, 1, 0, 0);
-       h->xlabel_changed = 0;
+       h->label_changed = 0;
       }
     }
   }
diff --git a/imap/message.c b/imap/message.c
--- a/imap/message.c
+++ b/imap/message.c
@@ -71,7 +71,7 @@
   int rc, mfhrc, oldmsgcount;
   int fetchlast = 0;
   int maxuid = 0;
-  const char *want_headers = "DATE FROM SUBJECT TO CC MESSAGE-ID REFERENCES 
CONTENT-TYPE CONTENT-DESCRIPTION IN-REPLY-TO REPLY-TO LINES LIST-POST X-LABEL";
+  const char *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;
 
@@ -404,7 +404,7 @@
   IMAP_CACHE *cache;
   int read;
   int rc;
-  char *x_label = NULL;
+
   /* 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
--- a/init.c
+++ b/init.c
@@ -3240,13 +3240,14 @@
 {
   char *pt = buffer;
   int spaces; /* keep track of the number of leading spaces on the line */
+  int prefix;
 
   SKIPWS (buffer);
   spaces = buffer - pt;
 
-  pt = buffer + pos - spaces;
-  while ((pt > buffer) && !isspace ((unsigned char) *pt))
-    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)
@@ -3284,7 +3285,7 @@
              Matches[(numtabs - 2) % Num_matched]);
 
   /* return the completed label */
-  strncpy (buffer, Completed, len - spaces);
+  strncpy (&buffer[prefix], Completed, len - spaces);
 
   return 1;
 }
diff --git a/init.h b/init.h
--- a/init.h
+++ b/init.h
@@ -1323,6 +1323,28 @@
   ** 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
@@ -3387,6 +3409,20 @@
   ** Also see the $$read_inc, $$net_inc and $$time_inc variables and the
   ** ``$tuning'' section of the manual for performance considerations.
   */
+  { "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 }
 };
diff --git a/mh.c b/mh.c
--- a/mh.c
+++ b/mh.c
@@ -1559,7 +1559,7 @@
 {
   HEADER *h = ctx->hdrs[msgno];
 
-  if (h->attach_del || h->xlabel_changed ||
+  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;
@@ -1571,7 +1571,7 @@
 {
   HEADER *h = ctx->hdrs[msgno];
 
-  if (h->attach_del || h->xlabel_changed ||
+  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. */
@@ -1693,7 +1693,7 @@
       }
     }
     else if (ctx->hdrs[i]->changed || ctx->hdrs[i]->attach_del ||
-            ctx->hdrs[i]->xlabel_changed ||
+            ctx->hdrs[i]->label_changed ||
             (ctx->magic == M_MAILDIR
              && (option (OPTMAILDIRTRASH) || ctx->hdrs[i]->trash)
              && (ctx->hdrs[i]->deleted != ctx->hdrs[i]->trash)))
diff --git a/mutt.h b/mutt.h
--- a/mutt.h
+++ b/mutt.h
@@ -311,6 +311,12 @@
 #define M_SPAM          1
 #define M_NOSPAM        2
 
+/* flags for keywords headers */
+#define M_X_LABEL         (1<<0)  /* introduced to mutt in 2000 */
+#define M_X_KEYWORDS      (1<<1)  /* used in c-client, dovecot */
+#define M_X_MOZILLA_KEYS  (1<<2)  /* tbird */
+#define M_KEYWORDS        (1<<3)  /* rfc2822 */
+
 /* boolean vars */
 enum
 {
@@ -384,6 +390,8 @@
   OPTIMPLICITAUTOVIEW,
   OPTINCLUDEONLYFIRST,
   OPTKEEPFLAGGED,
+  OPTKEYWORDSLEGACY,
+  OPTKEYWORDSSTANDARD,
   OPTMAILCAPSANITIZE,
   OPTMAILCHECKRECENT,
   OPTMAILDIRTRASH,
@@ -588,11 +596,12 @@
   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 */
@@ -725,7 +734,7 @@
                                         * This flag is used by the 
maildir_trash
                                         * option.
                                         */
-  unsigned int xlabel_changed : 1;     /* editable - used for syncing */
+  unsigned int label_changed : 1;      /* editable - used for syncing */
   
   /* timezone of the sender of this message */
   unsigned int zhours : 5;
diff --git a/muttlib.c b/muttlib.c
--- a/muttlib.c
+++ b/muttlib.c
@@ -721,13 +721,14 @@
   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__ */
 }
 
@@ -750,7 +751,7 @@
   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/parse.c b/parse.c
--- a/parse.c
+++ b/parse.c
@@ -971,6 +971,7 @@
 {
   int matched = 0;
   LIST *last = NULL;
+  int kwtype = 0;
   
   if (lastp)
     last = *lastp;
@@ -1077,7 +1078,14 @@
       matched = 1;
     }
     break;
-    
+
+    case 'k':
+    if (!ascii_strcasecmp (line+1, "eywords"))
+    {
+      kwtype = M_KEYWORDS;
+    }
+    break;
+
     case 'l':
     if (!ascii_strcasecmp (line + 1, "ines"))
     {
@@ -1252,10 +1260,17 @@
       }
       matched = 1;
     }
-    else if (ascii_strcasecmp (line+1, "-label") == 0)
+    else if (!ascii_strcasecmp (line+1, "-label"))
     {
-      e->x_label = safe_strdup(p);
-      matched = 1;
+      kwtype = M_X_LABEL;
+    }
+    else if (!ascii_strcasecmp (line+1, "-keywords"))
+    {
+      kwtype = M_X_KEYWORDS;
+    }
+    else if (!ascii_strcasecmp (line+1, "-mozilla-keys"))
+    {
+      kwtype = M_X_MOZILLA_KEYS;
     }
 
     if (last)
@@ -1270,12 +1285,59 @@
       rfc2047_decode (&last->data);
   }
 
+  if (kwtype)
+  {
+    char *last, *label;
+    char *text = strdup(p);
+    char *sep;
+
+    if (kwtype == M_KEYWORDS)
+      sep = ",";
+    else if (kwtype == M_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
  *
@@ -1418,7 +1480,6 @@
     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)
     {
diff --git a/pattern.c b/pattern.c
--- a/pattern.c
+++ b/pattern.c
@@ -1200,7 +1200,19 @@
        break;
      return (pat->not ^ ((h->security & APPLICATION_PGP) && (h->security & 
PGPKEY)));
     case M_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 M_HORMEL:
       return (pat->not ^ (h->env->spam && h->env->spam->data && patmatch (pat, 
h->env->spam->data) == 0));
     case M_DUPLICATED:
diff --git a/protos.h b/protos.h
--- a/protos.h
+++ b/protos.h
@@ -183,9 +183,12 @@
 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_path (char *, size_t, const char *);
diff --git a/sendlib.c b/sendlib.c
--- a/sendlib.c
+++ b/sendlib.c
@@ -2475,7 +2475,6 @@
   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)
   {
@@ -2500,7 +2499,6 @@
   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
--- a/sort.c
+++ b/sort.c
@@ -215,12 +215,13 @@
   HEADER **ppa = (HEADER **) a;
   HEADER **ppb = (HEADER **) b;
   int     ahas, bhas, result;
+  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->x_label && *((*ppa)->env->x_label);
-  bhas = (*ppb)->env && (*ppb)->env->x_label && *((*ppb)->env->x_label);
+  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)
@@ -236,7 +237,16 @@
   }
 
   /* If both have a label, we just do a lexical compare. */
-  result = mutt_strcasecmp((*ppa)->env->x_label, (*ppb)->env->x_label);
+  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));
 }