From: David Champion Date: Fri, 8 Apr 2016 00:08:05 +0000 (+0100) Subject: Unify label/keyword handling. X-Git-Tag: neomutt-20160530~9^2~6 X-Git-Url: https://granicus.if.org/sourcecode?a=commitdiff_plain;h=ea27e728e51c63bfbf8e221ad3c035bc177d8805;p=neomutt 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 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. --- diff --git a/copy.c b/copy.c index 2990d6ea8..231fb510b 100644 --- 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) diff --git a/doc/manual.xml.head b/doc/manual.xml.head index f556abe56..a720986d2 100644 --- a/doc/manual.xml.head +++ b/doc/manual.xml.head @@ -4532,8 +4532,8 @@ Mutt adds some other modifiers to format strings. If you use an equals symbol (=) as a numeric prefix (like the minus above), it will force the string to be centered within its minimum space range. For example, %=14y will reserve 14 characters -for the %y expansion — that's the X-Label: header, in $index_format. If the expansion results in +for the %y expansion — 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 test, that expansion would look like @@ -5126,7 +5126,7 @@ shows several ways to select messages. ~Vcryptographically verified messages ~x EXPRmessages which contain EXPR in the References or In-Reply-To field ~X [MIN]-[MAX]messages with MIN to MAX attachments *) -~y EXPRmessages which contain EXPR in the X-Label field +~y EXPRmessages which contain EXPR in their keywords ~z [MIN]-[MAX]messages with a size in the range MIN to MAX *) **) ~=duplicated messages (see $duplicate_threads) ~$unreferenced messages (requires threaded view) @@ -5977,27 +5977,6 @@ field. When set to yes, the Reply-To field will be used when present. - -The X-Label: header field can be used to further identify -mailing lists or list subject matter (or just to annotate messages -individually). The $index_format -variable's %y and %Y expandos can be used -to expand X-Label: fields in the index, and Mutt's -pattern-matcher can match regular expressions to X-Label: -fields with the ~y selector. X-Label: is -not a standard message header field, but it can easily be inserted by -procmail and other mail filtering agents. - - - -You can change or delete the X-Label: field within -Mutt using the edit-label command, bound to the -y key by default. This works for tagged messages, too. -While in the edit-label function, pressing the <complete> -binding (TAB, by default) will perform completion against all labels -currently in use. - - Lastly, Mutt has the ability to sort the mailbox into threads. A thread is a @@ -6011,6 +5990,121 @@ threads and quickly find topics of value. + +Keyword Management + + +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 keywords or tags. + + + +If you are new to Mutt or to using keywords in Mutt, you only need +to know that the <edit-label> binding (y by +default) will edit keywords, and that you can search for keywords +using the ~y pattern, and use the %y +expando to display it in your $index_format. You also +can sort by keyword. Keywords that you set will be stored to the +X-Label: header by default. + + + +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: + + + + + +X-Keywords + + +Informal design; space-delimited keywords + + + + + +X-Mozilla-Keys + + +Informal design used by Mozilla-based agents; space-delimited keywords + + + + + +Keywords + + +Standardized in RFC2822 (2001); comma-space-delimited keywords + + + + + +X-Label + + +Mutt-specific design; freeform text (but see $xlabel_delimiter) + + + + + + + +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. + + + +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. $keywords_legacy, if set, will tell +mutt to use only "legacy" headers -- i.e., X-Keywords:, +X-Mozilla-Keys, Keywords, or +X-Label:. Keywords will be saved to whichever +header was in use by the message the keyword was read from. If +$keywords_standard is +set, keywords will be saved without exception to the standard +Keywords: header. (If both are set, both will be used; +if both are unset, legacy headers are used.) Additionally, $xlabel_delimiter is used to format +the X-Label: header on saves. + + + +To migrate completely to the new standard, +unset $keywords_legacy and set +$keywords_standard, and set +$xlabel_delimiter either to what you currently use to +delimit keywords in X-Labels, or to (comma +space). + + + +Note that it is common practice to insert X-Label: 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. + + + + New Mail Detection diff --git a/globals.h b/globals.h index 6851e2bbd..f887b7488 100644 --- 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; diff --git a/hcache.c b/hcache.c index f4c0ecf41..d91b9c645 100644 --- 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 diff --git a/hdrline.c b/hdrline.c index b844411e5..60e3fe0ee 100644 --- 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; diff --git a/headers.c b/headers.c index a58105800..465bcf3af 100644 --- 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; +} diff --git a/imap/imap.c b/imap/imap.c index 2ce4db231..40b29e062 100644 --- a/imap/imap.c +++ b/imap/imap.c @@ -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; } } } diff --git a/imap/message.c b/imap/message.c index 98d5baf0c..f837ff057 100644 --- a/imap/message.c +++ b/imap/message.c @@ -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 2aa734251..e56993f93 100644 --- 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 749285434..8cc0b07cd 100644 --- 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 cd17f6181..6c3633ab7 100644 --- 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 51f575b8a..bfe485219 100644 --- 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; diff --git a/muttlib.c b/muttlib.c index a57dbf4a7..de6b58bb7 100644 --- 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 6a802baea..b02a044bb 100644 --- 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) { diff --git a/pattern.c b/pattern.c index d954cdc3b..9460fe640 100644 --- 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: diff --git a/protos.h b/protos.h index 018b2eeb2..488cc67a3 100644 --- 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); diff --git a/sendlib.c b/sendlib.c index e6b6a7932..f91c33452 100644 --- 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 7bc4aadd4..e27c35016 100644 --- 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)); }