changeset: 6912:67525605640e user: David Champion <d...@bikeshed.us> date: Sat Jan 28 18:47:15 2017 -0800 link: http://dev.mutt.org/hg/mutt/rev/67525605640e
Adds capability to edit x-labels inside mutt, and to sort by label. changeset: 6913:95b892b3f856 user: Kevin McCarthy <ke...@8t8.us> date: Sat Jan 28 18:47:26 2017 -0800 link: http://dev.mutt.org/hg/mutt/rev/95b892b3f856 Minor fixes to the x-label patch from David. Add L10N comment to sort menu. Mark a couple strings for localization. Use ascii_strncasecmp() for the X-Label header comparison. Simplify label_message() using mutt library routines. Bind label editing to "Y" instead of "y". "y" is already used in the default sample muttrc to display mailboxes. changeset: 6914:169b67b5b666 user: David Champion <d...@bikeshed.us> date: Sat Jan 28 18:47:41 2017 -0800 link: http://dev.mutt.org/hg/mutt/rev/169b67b5b666 Add reentrant hash_walk() function for iterating down a hash table. changeset: 6915:51c5e574a082 user: Kevin McCarthy <ke...@8t8.us> date: Sat Jan 28 18:47:48 2017 -0800 link: http://dev.mutt.org/hg/mutt/rev/51c5e574a082 Add hash_find_elem to get the hash element. This will be used in the following patch for directly manipulating the label counter. changeset: 6916:66cc205ea76a user: David Champion <d...@bikeshed.us> date: Sat Jan 28 18:47:57 2017 -0800 link: http://dev.mutt.org/hg/mutt/rev/66cc205ea76a Adds label completion. A global label hash is added, to which labels are added as they're parsed from a mailbox file or edited manually by the user. Reference counts are kept in the hash table so that unused labels are removed from available completions. Completion is available in the label editor only, but it may be feasible to add for search expressions if the preceding text ends with '~y'. changeset: 6917:d0909785d945 user: Kevin McCarthy <ke...@8t8.us> date: Sat Jan 28 18:48:08 2017 -0800 link: http://dev.mutt.org/hg/mutt/rev/d0909785d945 Improve the label completion hash table usage. Move the hash table inside the Context. Hook message arrival/deletion to update the label hash. Change the label hash to strdup keys. Use hash_find_elem when updating the counter, to reduce unnecessary add/delete operations. diffs (765 lines): diff -r e4ad1dc9bfbd -r d0909785d945 OPS --- a/OPS Tue Jan 24 15:33:23 2017 -0800 +++ b/OPS Sat Jan 28 18:48:08 2017 -0800 @@ -57,6 +57,7 @@ OP_DISPLAY_ADDRESS "display full address of sender" OP_DISPLAY_HEADERS "display message and toggle header weeding" OP_DISPLAY_MESSAGE "display a message" +OP_EDIT_LABEL "add, change, or delete a message's label" OP_EDIT_MESSAGE "edit the raw message" OP_EDITOR_BACKSPACE "delete the char in front of the cursor" OP_EDITOR_BACKWARD_CHAR "move the cursor one character to the left" diff -r e4ad1dc9bfbd -r d0909785d945 commands.c --- a/commands.c Tue Jan 24 15:33:23 2017 -0800 +++ b/commands.c Sat Jan 28 18:48:08 2017 -0800 @@ -533,9 +533,13 @@ int method = Sort; /* save the current method in case of abort */ switch (mutt_multi_choice (reverse ? - _("Rev-Sort (d)ate/(f)rm/(r)ecv/(s)ubj/t(o)/(t)hread/(u)nsort/si(z)e/s(c)ore/s(p)am?: ") : - _("Sort (d)ate/(f)rm/(r)ecv/(s)ubj/t(o)/(t)hread/(u)nsort/si(z)e/s(c)ore/s(p)am?: "), - _("dfrsotuzcp"))) + /* L10N: The following three are the sort/reverse sort prompts. + * Capital letters must match the order of the characters in the third + * string. + */ + _("Rev-Sort Date/Frm/Recv/Subj/tO/Thread/Unsort/siZe/sCore/sPam/Label?: ") : + _("Sort Date/Frm/Recv/Subj/tO/Thread/Unsort/siZe/sCore/sPam/Label?: "), + _("dfrsotuzcpl"))) { case -1: /* abort - don't resort */ return -1; @@ -579,6 +583,10 @@ case 10: /* s(p)am */ Sort = SORT_SPAM; break; + + case 11: /* (l)abel */ + Sort = SORT_LABEL; + break; } if (reverse) Sort |= SORT_REVERSE; diff -r e4ad1dc9bfbd -r d0909785d945 copy.c --- a/copy.c Tue Jan 24 15:33:23 2017 -0800 +++ b/copy.c Sat Jan 28 18:48:08 2017 -0800 @@ -111,6 +111,10 @@ ignore = 0; } + if (flags & CH_UPDATE_LABEL && + ascii_strncasecmp ("X-Label:", buf, 8) == 0) + continue; + if (!ignore && fputs (buf, out) == EOF) return (-1); } @@ -414,6 +418,15 @@ fprintf (out, "Lines: %d\n", h->lines); } + if (flags & CH_UPDATE_LABEL) + { + 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)) + return -1; + } + if ((flags & CH_NONEWLINE) == 0) { if (flags & CH_PREFIX) @@ -494,6 +507,9 @@ _mutt_make_string (prefix, sizeof (prefix), NONULL (Prefix), Context, hdr, 0); } + if (hdr->xlabel_changed) + chflags |= CH_UPDATE_LABEL; + if ((flags & MUTT_CM_NOHEADER) == 0) { if (flags & MUTT_CM_PREFIX) diff -r e4ad1dc9bfbd -r d0909785d945 copy.h --- a/copy.h Tue Jan 24 15:33:23 2017 -0800 +++ b/copy.h Sat Jan 28 18:48:08 2017 -0800 @@ -53,6 +53,7 @@ #define CH_UPDATE_IRT (1<<16) /* update In-Reply-To: */ #define CH_UPDATE_REFS (1<<17) /* update References: */ #define CH_DISPLAY (1<<18) /* display result to user */ +#define CH_UPDATE_LABEL (1<<19) /* update X-Label: from hdr->env->x_label? */ int mutt_copy_hdr (FILE *, FILE *, LOFF_T, LOFF_T, int, const char *); diff -r e4ad1dc9bfbd -r d0909785d945 curs_main.c --- a/curs_main.c Tue Jan 24 15:33:23 2017 -0800 +++ b/curs_main.c Sat Jan 28 18:48:08 2017 -0800 @@ -2093,6 +2093,26 @@ menu->redraw = REDRAW_FULL; break; + case OP_EDIT_LABEL: + + CHECK_MSGCOUNT; + CHECK_READONLY; + rc = mutt_label_message(tag ? NULL : CURHDR); + if (rc > 0) { + Context->changed = 1; + menu->redraw = REDRAW_FULL; + /* L10N: This is displayed when the x-label on one or more + * messages is edited. */ + mutt_message (_("%d labels changed."), rc); + } + else { + /* L10N: This is displayed when editing an x-label, but no messages + * were updated. Possibly due to canceling at the prompt or if the new + * label is the same as the old label. */ + mutt_message _("No labels changed."); + } + break; + case OP_LIST_REPLY: CHECK_ATTACH; diff -r e4ad1dc9bfbd -r d0909785d945 doc/manual.xml.head --- a/doc/manual.xml.head Tue Jan 24 15:33:23 2017 -0800 +++ b/doc/manual.xml.head Sat Jan 28 18:48:08 2017 -0800 @@ -548,7 +548,7 @@ <row><entry>^E or <End></entry><entry><literal><eol></literal></entry><entry>move to the end of the line</entry></row> <row><entry>^F or <Right></entry><entry><literal><forward-char></literal></entry><entry>move forward one char</entry></row> <row><entry>Esc F</entry><entry><literal><forward-word></literal></entry><entry>move forward one word</entry></row> -<row><entry><Tab></entry><entry><literal><complete></literal></entry><entry>complete filename or alias</entry></row> +<row><entry><Tab></entry><entry><literal><complete></literal></entry><entry>complete filename, alias, or label</entry></row> <row><entry>^T</entry><entry><literal><complete-query></literal></entry><entry>complete address with query</entry></row> <row><entry>^K</entry><entry><literal><kill-eol></literal></entry><entry>delete to the end of the line</entry></row> <row><entry>Esc d</entry><entry><literal><kill-eow></literal></entry><entry>delete to the end of the word</entry></row> @@ -6064,6 +6064,15 @@ </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 <complete> +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 group of messages which all relate to the same subject. This is usually diff -r e4ad1dc9bfbd -r d0909785d945 enter.c --- a/enter.c Tue Jan 24 15:33:23 2017 -0800 +++ b/enter.c Sat Jan 28 18:48:08 2017 -0800 @@ -566,6 +566,24 @@ } break; } + else if (flags & MUTT_LABEL && ch == OP_EDITOR_COMPLETE) + { + /* invoke the alias-menu to get more addresses */ + for (i = state->curpos; i && state->wbuf[i-1] != ',' && + state->wbuf[i-1] != ':'; i--) + ; + for (; i < state->lastchar && state->wbuf[i] == ' '; i++) + ; + my_wcstombs (buf, buflen, state->wbuf + i, state->curpos - i); + r = mutt_label_complete (buf, buflen, i, state->tabs); + replace_part (state, i, buf); + if (!r) + { + rv = 1; + goto bye; + } + break; + } else if (flags & MUTT_ALIAS && ch == OP_EDITOR_COMPLETE_QUERY) { /* invoke the query-menu to get more addresses */ diff -r e4ad1dc9bfbd -r d0909785d945 functions.h --- a/functions.h Tue Jan 24 15:33:23 2017 -0800 +++ b/functions.h Sat Jan 28 18:48:08 2017 -0800 @@ -99,6 +99,7 @@ { "delete-thread", OP_DELETE_THREAD, "\004" }, { "delete-subthread", OP_DELETE_SUBTHREAD, "\033d" }, { "edit", OP_EDIT_MESSAGE, "e" }, + { "edit-label", OP_EDIT_LABEL, "Y" }, { "edit-type", OP_EDIT_TYPE, "\005" }, { "forward-message", OP_FORWARD_MESSAGE, "f" }, { "flag-message", OP_FLAG_MESSAGE, "F" }, @@ -200,6 +201,7 @@ { "set-flag", OP_MAIN_SET_FLAG, "w" }, { "clear-flag", OP_MAIN_CLEAR_FLAG, "W" }, { "edit", OP_EDIT_MESSAGE, "e" }, + { "edit-label", OP_EDIT_LABEL, "Y" }, { "edit-type", OP_EDIT_TYPE, "\005" }, { "forward-message", OP_FORWARD_MESSAGE, "f" }, { "flag-message", OP_FLAG_MESSAGE, "F" }, diff -r e4ad1dc9bfbd -r d0909785d945 hash.c --- a/hash.c Tue Jan 24 15:33:23 2017 -0800 +++ b/hash.c Sat Jan 28 18:48:08 2017 -0800 @@ -172,7 +172,7 @@ return union_hash_insert (table, key, data, allow_dup); } -static void *union_hash_find (const HASH *table, union hash_key key) +static struct hash_elem *union_hash_find_elem (const HASH *table, union hash_key key) { int hash; struct hash_elem *ptr; @@ -185,11 +185,20 @@ for (; ptr; ptr = ptr->next) { if (table->cmp_key (key, ptr->key) == 0) - return (ptr->data); + return (ptr); } return NULL; } +static void *union_hash_find (const HASH *table, union hash_key key) +{ + struct hash_elem *ptr = union_hash_find_elem (table, key); + if (ptr) + return ptr->data; + else + return NULL; +} + void *hash_find (const HASH *table, const char *strkey) { union hash_key key; @@ -197,6 +206,13 @@ return union_hash_find (table, key); } +struct hash_elem *hash_find_elem (const HASH *table, const char *strkey) +{ + union hash_key key; + key.strkey = strkey; + return union_hash_find_elem (table, key); +} + void *int_hash_find (const HASH *table, unsigned int intkey) { union hash_key key; @@ -297,3 +313,30 @@ FREE (&pptr->table); FREE (ptr); /* __FREE_CHECKED__ */ } + +struct hash_elem *hash_walk(const HASH *table, struct hash_walk_state *state) +{ + if (state->last && state->last->next) + { + state->last = state->last->next; + return state->last; + } + + if (state->last) + state->index++; + + while (state->index < table->nelem) + { + if (table->table[state->index]) + { + state->last = table->table[state->index]; + return state->last; + } + state->index++; + } + + state->index = 0; + state->last = NULL; + return NULL; +} + diff -r e4ad1dc9bfbd -r d0909785d945 hash.h --- a/hash.h Tue Jan 24 15:33:23 2017 -0800 +++ b/hash.h Sat Jan 28 18:48:08 2017 -0800 @@ -53,6 +53,7 @@ int int_hash_insert (HASH *table, unsigned int key, void *data, int allow_dup); void *hash_find (const HASH *table, const char *key); +struct hash_elem *hash_find_elem (const HASH *table, const char *strkey); void *int_hash_find (const HASH *table, unsigned int key); struct hash_elem *hash_find_bucket (const HASH *table, const char *key); @@ -64,4 +65,11 @@ void hash_destroy (HASH ** hash, void (*destroy) (void *)); +struct hash_walk_state { + int index; + struct hash_elem *last; +}; + +struct hash_elem *hash_walk(const HASH *table, struct hash_walk_state *state); + #endif diff -r e4ad1dc9bfbd -r d0909785d945 headers.c --- a/headers.c Tue Jan 24 15:33:23 2017 -0800 +++ b/headers.c Sat Jan 28 18:48:08 2017 -0800 @@ -27,6 +27,7 @@ #include <sys/stat.h> #include <string.h> #include <ctype.h> +#include <stdint.h> void mutt_edit_headers (const char *editor, const char *body, @@ -211,3 +212,124 @@ } } } + +static void label_ref_dec(CONTEXT *ctx, char *label) +{ + struct hash_elem *elem; + uintptr_t count; + + elem = hash_find_elem (ctx->label_hash, label); + if (!elem) + return; + + count = (uintptr_t)elem->data; + if (count <= 1) + { + hash_delete(ctx->label_hash, label, NULL, NULL); + return; + } + + count--; + elem->data = (void *)count; +} + +static void label_ref_inc(CONTEXT *ctx, char *label) +{ + struct hash_elem *elem; + uintptr_t count; + + elem = hash_find_elem (ctx->label_hash, label); + if (!elem) + { + count = 1; + hash_insert(ctx->label_hash, label, (void *)count, 0); + return; + } + + count = (uintptr_t)elem->data; + count++; + elem->data = (void *)count; +} + +/* + * add an X-Label: field. + */ +static int label_message(CONTEXT *ctx, HEADER *hdr, char *new) +{ + if (hdr == NULL) + return 0; + if (mutt_strcmp (hdr->env->x_label, new) == 0) + return 0; + + if (hdr->env->x_label != NULL) + label_ref_dec(ctx, hdr->env->x_label); + mutt_str_replace (&hdr->env->x_label, new); + if (hdr->env->x_label != NULL) + label_ref_inc(ctx, hdr->env->x_label); + + return hdr->changed = hdr->xlabel_changed = 1; +} + +int mutt_label_message(HEADER *hdr) +{ + char buf[LONG_STRING], *new; + int i; + int changed; + + if (!Context || !Context->label_hash) + return 0; + + *buf = '\0'; + if (hdr != NULL && hdr->env->x_label != NULL) { + strncpy(buf, hdr->env->x_label, LONG_STRING); + } + + if (mutt_get_field("Label: ", buf, sizeof(buf), MUTT_LABEL /* | MUTT_CLEAR */) != 0) + return 0; + + new = buf; + SKIPWS(new); + if (*new == '\0') + new = NULL; + + changed = 0; + if (hdr != NULL) { + changed += label_message(Context, hdr, new); + } else { +#define HDR_OF(index) Context->hdrs[Context->v2r[(index)]] + for (i = 0; i < Context->vcount; ++i) { + if (HDR_OF(i)->tagged) + if (label_message(Context, HDR_OF(i), new)) { + ++changed; + mutt_set_flag(Context, HDR_OF(i), + MUTT_TAG, 0); + } + } + } + + return changed; +} + +void mutt_make_label_hash (CONTEXT *ctx) +{ + /* 131 is just a rough prime estimate of how many distinct + * labels someone might have in a mailbox. + */ + ctx->label_hash = hash_create(131, MUTT_HASH_STRDUP_KEYS); +} + +void mutt_label_hash_add (CONTEXT *ctx, HEADER *hdr) +{ + if (!ctx || !ctx->label_hash) + return; + if (hdr->env->x_label) + label_ref_inc (ctx, hdr->env->x_label); +} + +void mutt_label_hash_remove (CONTEXT *ctx, HEADER *hdr) +{ + if (!ctx || !ctx->label_hash) + return; + if (hdr->env->x_label) + label_ref_dec (ctx, hdr->env->x_label); +} diff -r e4ad1dc9bfbd -r d0909785d945 imap/imap.c --- a/imap/imap.c Tue Jan 24 15:33:23 2017 -0800 +++ b/imap/imap.c Sat Jan 28 18:48:08 2017 -0800 @@ -1242,7 +1242,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->attach_del || h->xlabel_changed) { mutt_message (_("Saving changed messages... [%d/%d]"), n+1, ctx->msgcount); @@ -1252,6 +1252,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; } } } diff -r e4ad1dc9bfbd -r d0909785d945 init.c --- a/init.c Tue Jan 24 15:33:23 2017 -0800 +++ b/init.c Sat Jan 28 18:48:08 2017 -0800 @@ -3629,3 +3629,60 @@ return NULL; } + +int mutt_label_complete (char *buffer, size_t len, int pos, int numtabs) +{ + char *pt = buffer; + int spaces; /* keep track of the number of leading spaces on the line */ + + if (!Context || !Context->label_hash) + return 0; + + SKIPWS (buffer); + spaces = buffer - pt; + + pt = buffer + pos - spaces; + while ((pt > buffer) && !isspace ((unsigned char) *pt)) + pt--; + + /* first TAB. Collect all the matches */ + if (numtabs == 1) + { + struct hash_elem *entry; + struct hash_walk_state state; + + Num_matched = 0; + strfcpy (User_typed, pt, sizeof (User_typed)); + memset (Matches, 0, Matches_listsize); + memset (Completed, 0, sizeof (Completed)); + memset (&state, 0, sizeof(state)); + while ((entry = hash_walk(Context->label_hash, &state))) + candidate (Completed, User_typed, entry->key.strkey, sizeof (Completed)); + matches_ensure_morespace (Num_matched); + qsort(Matches, Num_matched, sizeof(char *), (sort_t *) mutt_strcasecmp); + Matches[Num_matched++] = User_typed; + + /* All matches are stored. Longest non-ambiguous string is "" + * i.e. dont change 'buffer'. Fake successful return this time */ + if (User_typed[0] == 0) + return 1; + } + + if (Completed[0] == 0 && User_typed[0]) + return 0; + + /* Num_matched will _always_ be atleast 1 since the initial + * user-typed string is always stored */ + if (numtabs == 1 && Num_matched == 2) + snprintf(Completed, sizeof(Completed), "%s", Matches[0]); + else if (numtabs > 1 && Num_matched > 2) + /* cycle thru all the matches */ + snprintf(Completed, sizeof(Completed), "%s", + Matches[(numtabs - 2) % Num_matched]); + + /* return the completed label */ + strncpy (buffer, Completed, len - spaces); + + return 1; +} + diff -r e4ad1dc9bfbd -r d0909785d945 init.h --- a/init.h Tue Jan 24 15:33:23 2017 -0800 +++ b/init.h Sat Jan 28 18:48:08 2017 -0800 @@ -3813,6 +3813,7 @@ { "to", SORT_TO }, { "score", SORT_SCORE }, { "spam", SORT_SPAM }, + { "label", SORT_LABEL }, { NULL, 0 } }; @@ -3832,6 +3833,7 @@ { "to", SORT_TO }, { "score", SORT_SCORE }, { "spam", SORT_SPAM }, + { "label", SORT_LABEL }, { NULL, 0 } }; diff -r e4ad1dc9bfbd -r d0909785d945 mbox.c --- a/mbox.c Tue Jan 24 15:33:23 2017 -0800 +++ b/mbox.c Sat Jan 28 18:48:08 2017 -0800 @@ -1198,6 +1198,7 @@ hash_destroy (&ctx->id_hash, NULL); if (ctx->subj_hash) hash_destroy (&ctx->subj_hash, NULL); + hash_destroy (&ctx->label_hash, NULL); mutt_clear_threads (ctx); FREE (&ctx->v2r); if (ctx->readonly) @@ -1225,6 +1226,7 @@ ctx->changed = 0; ctx->id_hash = NULL; ctx->subj_hash = NULL; + mutt_make_label_hash (ctx); switch (ctx->magic) { diff -r e4ad1dc9bfbd -r d0909785d945 mh.c --- a/mh.c Tue Jan 24 15:33:23 2017 -0800 +++ b/mh.c Sat Jan 28 18:48:08 2017 -0800 @@ -1793,7 +1793,7 @@ { HEADER *h = ctx->hdrs[msgno]; - if (h->attach_del || + if (h->attach_del || h->xlabel_changed || (h->env && (h->env->refs_changed || h->env->irt_changed))) if (mh_rewrite_message (ctx, msgno) != 0) return -1; @@ -1805,7 +1805,7 @@ { HEADER *h = ctx->hdrs[msgno]; - if (h->attach_del || + if (h->attach_del || h->xlabel_changed || (h->env && (h->env->refs_changed || h->env->irt_changed))) { /* when doing attachment deletion/rethreading, fall back to the MH case. */ @@ -1927,6 +1927,7 @@ } } else if (ctx->hdrs[i]->changed || ctx->hdrs[i]->attach_del || + ctx->hdrs[i]->xlabel_changed || (ctx->magic == MUTT_MAILDIR && (option (OPTMAILDIRTRASH) || ctx->hdrs[i]->trash) && (ctx->hdrs[i]->deleted != ctx->hdrs[i]->trash))) diff -r e4ad1dc9bfbd -r d0909785d945 mutt.h --- a/mutt.h Tue Jan 24 15:33:23 2017 -0800 +++ b/mutt.h Sat Jan 28 18:48:08 2017 -0800 @@ -93,6 +93,7 @@ #define MUTT_CLEAR (1<<5) /* clear input if printable character is pressed */ #define MUTT_COMMAND (1<<6) /* do command completion */ #define MUTT_PATTERN (1<<7) /* pattern mode - only used for history classes */ +#define MUTT_LABEL (1<<8) /* do label completion */ /* flags for mutt_get_token() */ #define MUTT_TOKEN_EQUAL 1 /* treat '=' as a special */ @@ -760,6 +761,7 @@ * This flag is used by the maildir_trash * option. */ + unsigned int xlabel_changed : 1; /* editable - used for syncing */ /* timezone of the sender of this message */ unsigned int zhours : 5; @@ -953,6 +955,7 @@ HASH *id_hash; /* hash table by msg id */ HASH *subj_hash; /* hash table by subject */ HASH *thread_hash; /* hash table for threading */ + HASH *label_hash; /* hash table for x-labels */ int *v2r; /* mapping from virtual to real msgno */ int hdrmax; /* number of pointers in hdrs */ int msgcount; /* number of messages in the mailbox */ diff -r e4ad1dc9bfbd -r d0909785d945 mx.c --- a/mx.c Tue Jan 24 15:33:23 2017 -0800 +++ b/mx.c Sat Jan 28 18:48:08 2017 -0800 @@ -611,7 +611,9 @@ FREE (&ctx); return (NULL); } - + + mutt_make_label_hash (ctx); + /* if the user has a `push' command in their .muttrc, or in a folder-hook, * it will cause the progress messages not to be displayed because * mutt_refresh() will think we are in the middle of a macro. so set a @@ -680,6 +682,7 @@ hash_destroy (&ctx->subj_hash, NULL); if (ctx->id_hash) hash_destroy (&ctx->id_hash, NULL); + hash_destroy (&ctx->label_hash, NULL); mutt_clear_threads (ctx); for (i = 0; i < ctx->msgcount; i++) mutt_free_header (&ctx->hdrs[i]); @@ -1069,6 +1072,7 @@ hash_delete (ctx->subj_hash, ctx->hdrs[i]->env->real_subj, ctx->hdrs[i], NULL); if (ctx->id_hash && ctx->hdrs[i]->env->message_id) hash_delete (ctx->id_hash, ctx->hdrs[i]->env->message_id, ctx->hdrs[i], NULL); + mutt_label_hash_remove (ctx, ctx->hdrs[i]); /* The path mx_check_mailbox() -> imap_check_mailbox() -> * imap_expunge_mailbox() -> mx_update_tables() * can occur before a call to mx_sync_mailbox(), resulting in @@ -1419,6 +1423,7 @@ hash_insert (ctx->id_hash, h->env->message_id, h, 0); if (ctx->subj_hash && h->env->real_subj) hash_insert (ctx->subj_hash, h->env->real_subj, h, 1); + mutt_label_hash_add (ctx, h); if (option (OPTSCORE)) mutt_score_message (ctx, h, 0); diff -r e4ad1dc9bfbd -r d0909785d945 pager.c --- a/pager.c Tue Jan 24 15:33:23 2017 -0800 +++ b/pager.c Sat Jan 28 18:48:08 2017 -0800 @@ -2814,6 +2814,18 @@ redraw = REDRAW_FULL; break; + case OP_EDIT_LABEL: + CHECK_MODE(IsHeader (extra)); + rc = mutt_label_message(extra->hdr); + if (rc > 0) { + Context->changed = 1; + redraw = REDRAW_FULL; + mutt_message (_("%d labels changed."), rc); + } + else { + mutt_message _("No labels changed."); + } + break; case OP_MAIL_KEY: if (!(WithCrypto & APPLICATION_PGP)) diff -r e4ad1dc9bfbd -r d0909785d945 pop.c --- a/pop.c Tue Jan 24 15:33:23 2017 -0800 +++ b/pop.c Sat Jan 28 18:48:08 2017 -0800 @@ -632,10 +632,12 @@ /* we replace envelop, key in subj_hash has to be updated as well */ if (ctx->subj_hash && h->env->real_subj) hash_delete (ctx->subj_hash, h->env->real_subj, h, NULL); + mutt_label_hash_remove (ctx, h); mutt_free_envelope (&h->env); h->env = mutt_read_rfc822_header (msg->fp, h, 0, 0); if (ctx->subj_hash && h->env->real_subj) hash_insert (ctx->subj_hash, h->env->real_subj, h, 1); + mutt_label_hash_add (ctx, h); h->data = uidl; h->lines = 0; diff -r e4ad1dc9bfbd -r d0909785d945 protos.h --- a/protos.h Tue Jan 24 15:33:23 2017 -0800 +++ b/protos.h Sat Jan 28 18:48:08 2017 -0800 @@ -186,6 +186,11 @@ 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 **); +int mutt_label_message (HEADER *); +void mutt_make_label_hash (CONTEXT *); +void mutt_label_hash_add (CONTEXT *ctx, HEADER *hdr); +void mutt_label_hash_remove (CONTEXT *ctx, HEADER *hdr); +int mutt_label_complete (char *, size_t, int, int); void mutt_curses_error (const char *, ...); void mutt_curses_message (const char *, ...); void mutt_encode_descriptions (BODY *, short); diff -r e4ad1dc9bfbd -r d0909785d945 sort.c --- a/sort.c Tue Jan 24 15:33:23 2017 -0800 +++ b/sort.c Sat Jan 28 18:48:08 2017 -0800 @@ -210,6 +210,36 @@ return (SORTCODE(result)); } +int compare_label (const void *a, const void *b) +{ + HEADER **ppa = (HEADER **) a; + HEADER **ppb = (HEADER **) b; + int ahas, bhas, result = 0; + + /* 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); + + /* First we bias toward a message with a label, if the other does not. */ + if (ahas && !bhas) + return (SORTCODE(-1)); + if (!ahas && bhas) + return (SORTCODE(1)); + + /* If neither has a label, use aux sort. */ + if (!ahas && !bhas) + { + AUXSORT(result, a, b); + return (SORTCODE(result)); + } + + /* If both have a label, we just do a lexical compare. */ + result = mutt_strcasecmp((*ppa)->env->x_label, (*ppb)->env->x_label); + return (SORTCODE(result)); +} + sort_t *mutt_get_sort_func (int method) { switch (method & SORT_MASK) @@ -232,6 +262,8 @@ return (compare_score); case SORT_SPAM: return (compare_spam); + case SORT_LABEL: + return (compare_label); default: return (NULL); } diff -r e4ad1dc9bfbd -r d0909785d945 sort.h --- a/sort.h Tue Jan 24 15:33:23 2017 -0800 +++ b/sort.h Sat Jan 28 18:48:08 2017 -0800 @@ -35,6 +35,7 @@ #define SORT_UNREAD 16 #define SORT_FLAGGED 17 #define SORT_PATH 18 +#define SORT_LABEL 19 /* Sort and sort_aux are shorts, and are a composite of a * constant sort operation number and a set of compounded