]> granicus.if.org Git - neomutt/commitdiff
Unify label/keyword handling.
authorDavid Champion <dgc@bikeshed.us>
Fri, 8 Apr 2016 00:08:05 +0000 (01:08 +0100)
committerRichard Russon <rich@flatcap.org>
Tue, 17 May 2016 16:27:48 +0000 (17:27 +0100)
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 can 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 two booleans: $keywords_legacy=true and
$keywords_standard=FALSE.  When $keywords_legacy is true, mutt will
always save keyword to whatever original header it came from.  When
$keywords_standard=true, mutt will save to the Keywords: header.  If
both are true mutt saves to both; if neither is true, mutt saves only
to legacy headers to avoid complete loss of keywords.

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

18 files changed:
copy.c
doc/manual.xml.head
globals.h
hcache.c
hdrline.c
headers.c
imap/imap.c
imap/message.c
init.c
init.h
mh.c
mutt.h
muttlib.c
parse.c
pattern.c
protos.h
sendlib.c
sort.c

diff --git a/copy.c b/copy.c
index 2990d6ea8d8fc9a8b96e2566c93a8a2f34a76c47..231fb510b0ef87e2553a15e71d2c498812a4708d 100644 (file)
--- a/copy.c
+++ b/copy.c
@@ -111,9 +111,14 @@ mutt_copy_hdr (FILE *in, FILE *out, LOFF_T off_start, LOFF_T off_end, int flags,
        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);
@@ -417,13 +422,58 @@ 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->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)
@@ -506,7 +556,7 @@ _mutt_copy_message (FILE *fpout, FILE *fpin, HEADER *hdr, BODY *body,
       _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)
index f556abe56657c512798e2ddaf8c106e106c9767b..a720986d2e57da12d765da2f051686cac99fc3a2 100644 (file)
@@ -4532,8 +4532,8 @@ Mutt adds some other modifiers to format strings. If you use an equals
 symbol (<literal>=</literal>) as a numeric prefix (like the minus
 above), it will force the string to be centered within its minimum space
 range. For example, <literal>%=14y</literal> will reserve 14 characters
-for the %y expansion &mdash; that's the X-Label: header, in <link
-linkend="index-format">$index_format</link>. If the expansion results in
+for the %y expansion &mdash; that's the set of message keywords (formerly
+X-Label).  If the expansion results in
 a string less than 14 characters, it will be centered in a 14-character
 space.  If the X-Label for a message were <quote>test</quote>, that
 expansion would look like
@@ -5126,7 +5126,7 @@ shows several ways to select messages.
 <row><entry>~V</entry><entry>cryptographically verified messages</entry></row>
 <row><entry>~x <emphasis>EXPR</emphasis></entry><entry>messages which contain <emphasis>EXPR</emphasis> in the <quote>References</quote> or <quote>In-Reply-To</quote> field</entry></row>
 <row><entry>~X [<emphasis>MIN</emphasis>]-[<emphasis>MAX</emphasis>]</entry><entry>messages with <emphasis>MIN</emphasis> to <emphasis>MAX</emphasis> attachments *)</entry></row>
-<row><entry>~y <emphasis>EXPR</emphasis></entry><entry>messages which contain <emphasis>EXPR</emphasis> in the <quote>X-Label</quote> field</entry></row>
+<row><entry>~y <emphasis>EXPR</emphasis></entry><entry>messages which contain <emphasis>EXPR</emphasis> in their keywords</entry></row>
 <row><entry>~z [<emphasis>MIN</emphasis>]-[<emphasis>MAX</emphasis>]</entry><entry>messages with a size in the range <emphasis>MIN</emphasis> to <emphasis>MAX</emphasis> *) **)</entry></row>
 <row><entry>~=</entry><entry>duplicated messages (see <link linkend="duplicate-threads">$duplicate_threads</link>)</entry></row>
 <row><entry>~$</entry><entry>unreferenced messages (requires threaded view)</entry></row>
@@ -5977,27 +5977,6 @@ field.  When set to <emphasis>yes</emphasis>, the
 <quote>Reply-To</quote> field will be used when present.
 </para>
 
-<para>
-The <quote>X-Label:</quote> header field can be used to further identify
-mailing lists or list subject matter (or just to annotate messages
-individually).  The <link linkend="index-format">$index_format</link>
-variable's <quote>%y</quote> and <quote>%Y</quote> expandos can be used
-to expand <quote>X-Label:</quote> fields in the index, and Mutt's
-pattern-matcher can match regular expressions to <quote>X-Label:</quote>
-fields with the <quote>~y</quote> selector.  <quote>X-Label:</quote> is
-not a standard message header field, but it can easily be inserted by
-procmail and other mail filtering agents.
-</para>
-
-<para>
-You can change or delete the <quote>X-Label:</quote> field within
-Mutt using the <quote>edit-label</quote> command, bound to the
-<quote>y</quote> key by default.  This works for tagged messages, too.
-While in the edit-label function, pressing the &lt;complete&gt;
-binding (TAB, by default) will perform completion against all labels
-currently in use.
-</para>
-
 <para>
 Lastly, Mutt has the ability to <link linkend="sort">sort</link> the
 mailbox into <link linkend="threads">threads</link>.  A thread is a
@@ -6011,6 +5990,121 @@ threads and quickly find topics of value.
 
 </sect1>
 
+<sect1 id="using-keywords">
+<title>Keyword Management</title>
+
+<para>
+Mutt has supported textual labels (usually known as X-Labels after
+the header that we use to store them) for many years.  Since we
+initially added support for X-Lanels, however, the larger community
+has evolved more common ways of using and managing such labels, often
+known as <quote>keywords</quote> or <quote>tags</quote>.
+</para>
+
+<para>
+If you are new to Mutt or to using keywords in Mutt, you only need
+to know that the &lt;edit-label&gt; binding (<literal>y</literal> by
+default) will edit keywords, and that you can search for keywords
+using the <literal>~y</literal> pattern, and use the <literal>%y</literal>
+expando to display it in your <literal>$index_format</literal>.  You also
+can sort by keyword.  Keywords that you set will be stored to the
+<literal>X-Label:</literal> header by default.
+</para>
+
+<para>
+If you've been using X-Labels for a while, things have grown slightly.
+Mutt still supports X-Labels much as it has since 2000, but the scope
+of this support has expanded to support three additional header-based
+techniques for storing keyword metadata on messages:
+</para>
+
+<variablelist>
+
+<varlistentry>
+<term>X-Keywords</term>
+<listitem>
+<para>
+Informal design; space-delimited keywords
+</para>
+</listitem>
+</varlistentry>
+
+<varlistentry>
+<term>X-Mozilla-Keys</term>
+<listitem>
+<para>
+Informal design used by Mozilla-based agents; space-delimited keywords
+</para>
+</listitem>
+</varlistentry>
+
+<varlistentry>
+<term>Keywords</term>
+<listitem>
+<para>
+Standardized in RFC2822 (2001); comma-space-delimited keywords
+</para>
+</listitem>
+</varlistentry>
+
+<varlistentry>
+<term>X-Label</term>
+<listitem>
+<para>
+Mutt-specific design; freeform text (but see <link linkend="xlabel-delimiter">$xlabel_delimiter</link>)
+</para>
+</listitem>
+</varlistentry>
+
+</variablelist>
+
+<para>
+With X-Label, mutt's only notion of a message keyword was the literal
+string value of the X-Label header.  Under the new, integrated support,
+each message may have a list of distinct message keywords.  When reading
+keywords from one of the headers in the list above, the header value is
+split on the indicated delimiter (space or comma-space) for X-Keywords:,
+X-Mozilla-Keys:, and Keywords:.  By default, X-Label: is parsed as a
+single keyword.  By setting $xlabel_delimiter, you can force splitting
+of X-Label: as well.
+</para>
+
+<para>
+Two boolean variables control how keywords are saved when writing
+messages to a mailbox.  The default settings preserve backward
+compatibility within mutt completely, but by changing these
+values you can transition to more standard keyword storage. <link
+linkend="keywords-legacy">$keywords_legacy</link>, if set, will tell
+mutt to use only "legacy" headers -- i.e., <literal>X-Keywords:</literal>,
+<literal>X-Mozilla-Keys</literal>, <literal>Keywords</literal>, or
+<literal>X-Label:</literal>.  Keywords will be saved to whichever
+header was in use by the message the keyword was read from.  If
+<link linkend="keywords-standard">$keywords_standard</link> is
+set, keywords will be saved without exception to the standard
+<literal>Keywords:</literal> header.  (If both are set, both will be used;
+if both are unset, legacy headers are used.)  Additionally, <link
+linkend="xlabel-delimiter">$xlabel_delimiter</link> is used to format
+the X-Label: header on saves.
+</para>
+
+<para>
+To migrate completely to the new standard,
+unset <literal>$keywords_legacy</literal> and set
+<literal>$keywords_standard</literal>, and set
+<literal>$xlabel_delimiter</literal> either to what you currently use to
+delimit keywords in X-Labels, or to <quote>,&nbsp;</quote> (comma
+space).
+</para>
+
+<para>
+Note that it is common practice to insert <literal>X-Label:</literal> or
+other keyword headers from proxmail or other mail filters.  This is
+a useful trick for categorizing messages en masse as they are delivered
+to your inbox, and it is fully compatible with the new keywords code.
+</para>
+
+</sect1>
+
 <sect1 id="new-mail">
 <title>New Mail Detection</title>
 
index 6851e2bbd2d7abef4782f146a6450fb5f3652f18..f887b748831c72356b3bcf67ede4c59abc34d3ea 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;
@@ -146,6 +147,7 @@ WHERE char *TSIconFormat;
 WHERE short TSSupported;
 WHERE char *Username;
 WHERE char *Visual;
+WHERE char *XlabelDelim;
 
 WHERE char *CurrentFolder;
 WHERE char *LastFolder;
index f4c0ecf417d0917ff481f376c6222c75b371acc2..d91b9c6453909a7a320429533d15e82784554c82 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 b844411e5f5e29cac4311c9dc85c2e1e66457c5b..60e3fe0eeb2e50ef40798faa3d14cc40a9530a55 100644 (file)
--- a/hdrline.c
+++ b/hdrline.c
@@ -694,38 +694,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 & 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;
-
-      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 & 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;
+
+        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 a5810580056001ff62f0a1816bccf25073b3d22b..465bcf3af4839c654e6b3c839eb7561ee840ba49 100644 (file)
--- a/headers.c
+++ b/headers.c
@@ -212,58 +212,91 @@ void mutt_edit_headers (const char *editor,
   }
 }
 
-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)
-    return 0;
-  if (hdr->env->x_label != NULL && new != NULL &&
-      strcmp(hdr->env->x_label, new) == 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->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)
@@ -273,15 +306,31 @@ int mutt_label_message(HEADER *hdr)
   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;
 
@@ -309,7 +358,43 @@ void mutt_scan_labels(CONTEXT *ctx)
   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;
+}
index 2ce4db231de4981a7b70a15c41b77c19a861cdeb..40b29e062093cfb38945522dbd2238b15eca0465 100644 (file)
@@ -1222,7 +1222,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->xlabel_changed)
+         h->attach_del || h->label_changed)
       {
         mutt_message (_("Saving changed messages... [%d/%d]"), n+1,
                       ctx->msgcount);
@@ -1232,7 +1232,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->xlabel_changed = 0;
+       h->label_changed = 0;
       }
     }
   }
index 98d5baf0c0fa0c702fc373ee9d686113cb0b9870..f837ff057c79352f86bf1df11ea652d06a21d532 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;
 
@@ -406,7 +406,7 @@ int imap_fetch_message (MESSAGE *msg, CONTEXT *ctx, int msgno)
   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
index 2aa7342511da4f8d1253ec54e86f793bf30bcece..e56993f9345c6c6b372c87e311728e364c2fc111 100644 (file)
--- a/init.c
+++ b/init.c
@@ -3287,13 +3287,14 @@ 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;
 
-  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)
@@ -3331,7 +3332,7 @@ int mutt_label_complete (char *buffer, size_t len, int pos, int numtabs)
              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
index 749285434d0a10d04b624efd52bbcae6333f9ee4..8cc0b07cda2fe716234253d93bd8f88f7d4df1cf 100644 (file)
--- a/init.h
+++ b/init.h
@@ -1384,6 +1384,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
@@ -3590,6 +3612,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 }
 };
diff --git a/mh.c b/mh.c
index cd17f6181e6ba5fcf88cde97507df8b9bf6d3fe2..6c3633ab7346d5b0ca2c8680bc25e36327f35bdd 100644 (file)
--- a/mh.c
+++ b/mh.c
@@ -1612,7 +1612,7 @@ static int mh_sync_message (CONTEXT * ctx, int msgno)
 {
   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;
@@ -1624,7 +1624,7 @@ static int maildir_sync_message (CONTEXT * ctx, int msgno)
 {
   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. */
@@ -1746,7 +1746,7 @@ int mh_sync_mailbox (CONTEXT * ctx, int *index_hint)
       }
     }
     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
index 51f575b8a09d374a7a7794f11ea87e10dc0bb0bc..bfe48521901eb136ee2bfbff49117dd7c99df04c 100644 (file)
--- a/mutt.h
+++ b/mutt.h
@@ -312,6 +312,12 @@ enum
 #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
 {
@@ -387,6 +393,8 @@ enum
   OPTIMPLICITAUTOVIEW,
   OPTINCLUDEONLYFIRST,
   OPTKEEPFLAGGED,
+  OPTKEYWORDSLEGACY,
+  OPTKEYWORDSSTANDARD,
   OPTMAILCAPSANITIZE,
   OPTMAILCHECKRECENT,
   OPTMAILDIRTRASH,
@@ -600,11 +608,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 */
@@ -737,7 +746,7 @@ typedef struct header
                                         * 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;
index a57dbf4a790028ee8b9b5103560c3cb7eca127e5..de6b58bb7a7d449306ffc8b40dafceb4f2aa594a 100644 (file)
--- a/muttlib.c
+++ b/muttlib.c
@@ -713,13 +713,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__ */
 }
 
@@ -742,7 +743,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/parse.c b/parse.c
index 6a802baeadab3e4d9360bad4346170de59c7d026..b02a044bbe3c2ff9129ad0465c9bd18760c44678 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 = M_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 = 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;
+    }
+
     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 = 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
  *
@@ -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 d954cdc3bbd7a23b41360ed7f74ae55d0bf73e87..9460fe640c696e82d4e8c69431ae029edda6eefb 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 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:
index 018b2eeb2507a41a395ac3dc98c5cf99dc5353a0..488cc67a335c24115a83422be88c3543074117dd 100644 (file)
--- a/protos.h
+++ b/protos.h
@@ -184,9 +184,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 e6b6a79325ac2aa4bb2615e9b3566835cb64883a..f91c33452c3ea55de9839aecba3386003df9fe84 100644 (file)
--- a/sendlib.c
+++ b/sendlib.c
@@ -2489,7 +2489,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)
   {
@@ -2514,7 +2513,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 7bc4aadd4045b641fb9f03588c41d276af267d6f..e27c35016518381c29742ab06051810877794b69 100644 (file)
--- a/sort.c
+++ b/sort.c
@@ -215,12 +215,13 @@ int compare_label (const void *a, const void *b)
   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 @@ int compare_label (const void *a, const void *b)
   }
 
   /* 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));
 }