]> granicus.if.org Git - neomutt/commitdiff
Add thread editing commands.
authorCedric Duval <cedricduval+web@free.fr>
Sun, 24 Jul 2005 16:51:38 +0000 (16:51 +0000)
committerCedric Duval <cedricduval+web@free.fr>
Sun, 24 Jul 2005 16:51:38 +0000 (16:51 +0000)
12 files changed:
OPS
copy.c
curs_main.c
doc/manual.sgml.head
functions.h
imap/imap.c
mh.c
mutt.h
mx.c
pager.c
protos.h
thread.c

diff --git a/OPS b/OPS
index 0844487f8bf21b99d85c642fd73e545281a6b5b5..1db524a8a1da24becf801591a41cabd5b74a72b4 100644 (file)
--- a/OPS
+++ b/OPS
@@ -96,6 +96,7 @@ OP_LAST_ENTRY "move to the last entry"
 OP_LIST_REPLY "reply to specified mailing list"
 OP_MACRO "execute a macro"
 OP_MAIL "compose a new mail message"
+OP_MAIN_BREAK_THREAD "break the thread in two"
 OP_MAIN_CHANGE_FOLDER "open a different folder"
 OP_MAIN_CHANGE_FOLDER_READONLY "open a different folder in read only mode"
 OP_MAIN_CLEAR_FLAG "clear a status flag from a message"
@@ -105,6 +106,7 @@ OP_MAIN_FETCH_MAIL "retrieve mail from POP server"
 OP_MAIN_FIRST_MESSAGE "move to the first message"
 OP_MAIN_LAST_MESSAGE "move to the last message"
 OP_MAIN_LIMIT "show only messages matching a pattern"
+OP_MAIN_LINK_THREADS "link tagged message to the current one"
 OP_MAIN_NEXT_NEW "jump to the next new message"
 OP_MAIN_NEXT_NEW_THEN_UNREAD "jump to the next new or unread message"
 OP_MAIN_NEXT_SUBTHREAD "jump to the next subthread"
diff --git a/copy.c b/copy.c
index d1b3f7c2d925eb1722ba9ec2e34c4e5718298ece..60ae50e9e92dfd7c461b404d91af9deae15e56cd 100644 (file)
--- a/copy.c
+++ b/copy.c
@@ -99,6 +99,12 @@ mutt_copy_hdr (FILE *in, FILE *out, long off_start, long off_end, int flags,
            (ascii_strncasecmp ("Content-Length:", buf, 15) == 0 ||
             ascii_strncasecmp ("Lines:", buf, 6) == 0))
          continue;
+       if ((flags & CH_UPDATE_REFS) &&
+           ascii_strncasecmp ("References:", buf, 11) == 0)
+         continue;
+       if ((flags & CH_UPDATE_IRT) &&
+           ascii_strncasecmp ("In-Reply-To:", buf, 12) == 0)
+         continue;
        ignore = 0;
       }
 
@@ -197,6 +203,12 @@ mutt_copy_hdr (FILE *in, FILE *out, long off_start, long off_end, int flags,
             ascii_strncasecmp ("type:", buf + 8, 5) == 0)) ||
           ascii_strncasecmp ("mime-version:", buf, 13) == 0))
        continue;
+      if ((flags & CH_UPDATE_REFS) &&
+         ascii_strncasecmp ("References:", buf, 11) == 0)
+       continue;
+      if ((flags & CH_UPDATE_IRT) &&
+         ascii_strncasecmp ("In-Reply-To:", buf, 12) == 0)
+       continue;
 
       /* Find x -- the array entry where this header is to be saved */
       if (flags & CH_REORDER)
@@ -330,6 +342,8 @@ mutt_copy_hdr (FILE *in, FILE *out, long off_start, long off_end, int flags,
        CH_XMIT         ignore Lines: and Content-Length:
        CH_WEED         do header weeding
        CH_NOQFROM      ignore ">From " line
+       CH_UPDATE_IRT   update the In-Reply-To: header
+       CH_UPDATE_REFS  update the References: header
 
    prefix
        string to use if CH_PREFIX is set
@@ -339,6 +353,9 @@ int
 mutt_copy_header (FILE *in, HEADER *h, FILE *out, int flags, const char *prefix)
 {
   char buffer[SHORT_STRING];
+
+  flags |= (h->irt_changed ? CH_UPDATE_IRT : 0)
+         | (h->refs_changed ? CH_UPDATE_REFS : 0);
   
   if (mutt_copy_hdr (in, out, h->offset, h->content->offset, flags, prefix) == -1)
     return (-1);
@@ -362,7 +379,56 @@ mutt_copy_header (FILE *in, HEADER *h, FILE *out, int flags, const char *prefix)
   if (flags & CH_UPDATE)
   {
     if ((flags & CH_NOSTATUS) == 0)
+#ifdef USE_IMAP
+#define NEW_ENV new_env
+#else
+#define NEW_ENV env
+#endif
     {
+      if (h->irt_changed && h->NEW_ENV->in_reply_to)
+      {
+       LIST *listp = h->NEW_ENV->in_reply_to;
+
+       if (fputs ("In-Reply-To: ", out) == EOF)
+         return (-1);
+
+       for (; listp; listp = listp->next)
+         if ((fputs (listp->data, out) == EOF) || (fputc (' ', out) == EOF))
+           return (-1);
+
+       if (fputc ('\n', out) == EOF)
+         return (-1);
+      }
+
+      if (h->refs_changed && h->NEW_ENV->references)
+      {
+       LIST *listp = h->NEW_ENV->references, *refs = NULL, *t;
+
+       if (fputs ("References: ", out) == EOF)
+         return (-1);
+
+       /* Mutt stores references in reverse order, thus we create
+        * a reordered refs list that we can put in the headers */
+       for (; listp; listp = listp->next, refs = t)
+       {
+         t = (LIST *)safe_malloc (sizeof (LIST));
+         t->data = listp->data;
+         t->next = refs;
+       }
+
+       for (; refs; refs = refs->next)
+         if ((fputs (refs->data, out) == EOF) || (fputc (' ', out) == EOF))
+           return (-1);
+
+       /* clearing refs from memory */
+       for (t = refs; refs; refs = t->next, t = refs)
+         safe_free ((void **)&refs);
+
+       if (fputc ('\n', out) == EOF)
+         return (-1);
+      }
+#undef NEW_ENV
+
       if (h->old || h->read)
       {
        if (fputs ("Status: ", out) == EOF)
index c2b7c8c718ecbf93a818c4c1e41af5b48c2753e5..1d96c891552bc22648b62bf69641a09bd86d653f 100644 (file)
@@ -936,6 +936,11 @@ CHECK_IMAP_ACL(IMAP_ACL_DELETE);
        else
        {
          mutt_set_flag (Context, CURHDR, M_TAG, !CURHDR->tagged);
+
+         Context->last_tag = CURHDR->tagged ? CURHDR :
+           ((Context->last_tag == CURHDR && !CURHDR->tagged)
+            ? NULL : Context->last_tag);
+
          menu->redraw = REDRAW_STATUS;
          if (option (OPTRESOLVE) && menu->current < Context->vcount - 1)
          {
@@ -1176,6 +1181,77 @@ CHECK_IMAP_ACL(IMAP_ACL_DELETE);
        }
        break;
 
+      case OP_MAIN_BREAK_THREAD:
+
+       CHECK_MSGCOUNT;
+        CHECK_VISIBLE;
+       CHECK_READONLY;
+
+        if ((Sort & SORT_MASK) != SORT_THREADS)
+         mutt_error _("Threading is not enabled.");
+       else
+       {
+         {
+           HEADER *oldcur = CURHDR;
+
+           mutt_break_thread (CURHDR);
+           mutt_sort_headers (Context, 1);
+           menu->current = oldcur->virtual;
+         }
+
+         Context->changed = 1;
+         mutt_message _("Thread broken");
+
+         if (menu->menu == MENU_PAGER)
+         {
+           op = OP_DISPLAY_MESSAGE;
+           continue;
+         }
+         else
+           menu->redraw |= REDRAW_INDEX;
+       }
+
+         break;
+
+      case OP_MAIN_LINK_THREADS:
+
+       CHECK_MSGCOUNT;
+        CHECK_VISIBLE;
+       CHECK_READONLY;
+
+        if ((Sort & SORT_MASK) != SORT_THREADS)
+         mutt_error _("Threading is not enabled.");
+       else if (!CURHDR->env->message_id)
+         mutt_error _("No Message-ID: header available to link thread");
+       else if (!tag && (!Context->last_tag || !Context->last_tag->tagged))
+         mutt_error _("First, please tag a message to be linked here");
+       else 
+       {
+         HEADER *oldcur = CURHDR;
+
+         if (mutt_link_threads (CURHDR, tag ? NULL : Context->last_tag,
+                                Context))
+         {
+           mutt_sort_headers (Context, 1);
+           menu->current = oldcur->virtual;
+           
+           Context->changed = 1;
+           mutt_message _("Threads linked");
+         }
+         else
+           mutt_error _("No thread linked");
+       }
+
+       if (menu->menu == MENU_PAGER)
+       {
+         op = OP_DISPLAY_MESSAGE;
+         continue;
+       }
+       else
+         menu->redraw |= REDRAW_STATUS | REDRAW_INDEX;
+
+       break;
+
       case OP_EDIT_TYPE:
 
        CHECK_MSGCOUNT;
index 87be7dc1836ae152eec5d0b380acc7be5f08cba3..4c0ebcc5ef1cf23cc1b45721a8b877c2e5b3f1f2 100644 (file)
@@ -2334,8 +2334,43 @@ used a threaded news client, this is the same concept.  It makes dealing
 with large volume mailing lists easier because you can easily delete
 uninteresting threads and quickly find topics of value.
 
+<sect1>Editing threads
+<p>
+Mutt has the ability to dynamically restructure threads that are broken
+either by misconfigured software or bad behaviour from some
+correspondents. This allows to clean your mailboxes formats) from these
+annoyances which make it hard to follow a discussion.
+
+If you want to use these functions with IMAP, you need to compile Mutt
+with the <em/--enable-imap-edit-threads/ configure flag.
+
+<sect2>Linking threads
+<p>
+
+Some mailers tend to "forget" to correctly set the "In-Reply-To:" and
+"References:" headers when replying to a message. This results in broken
+discussions because Mutt has not enough information to guess the correct
+threading.
+You can fix this by tagging the reply, then moving to the parent message
+and using the ``link-threads'' function (bound to & by default). The
+reply will then be connected to this "parent" message.
+
+You can also connect multiple childs at once, tagging them and using the
+tag-prefix command (';') or the auto_tag option.
+
+<sect2>Breaking threads
+<p>
+
+On mailing lists, some people are in the bad habit of starting a new
+discussion by hitting "reply" to any message from the list and changing
+the subject to a totally unrelated one.
+You can fix such threads by using the ``break-thread'' function (bound
+by default to #), which will turn the subthread starting from the
+current message into a whole different thread.
+
 <sect1>Delivery Status Notification (DSN) Support
 <p>
+
 RFC1894 defines a set of MIME content types for relaying information
 about the status of electronic mail messages.  These can be thought of as
 ``return receipts.'' Berkeley sendmail 8.8.x currently has some command
index 47181cc765a9abc9dc63347625a5e074b3dc2b7f..6e1be51a7486b503d58d6aaff7866f9df4bb30c0 100644 (file)
@@ -69,6 +69,7 @@ struct binding_t OpGeneric[] = {
 struct binding_t OpMain[] = {
   { "create-alias",            OP_CREATE_ALIAS,                "a" },
   { "bounce-message",          OP_BOUNCE_MESSAGE,              "b" },
+  { "break-thread",            OP_MAIN_BREAK_THREAD,           "#" },
   { "change-folder",           OP_MAIN_CHANGE_FOLDER,          "c" },
   { "change-folder-readonly",  OP_MAIN_CHANGE_FOLDER_READONLY, "\033c" },
   { "collapse-thread",         OP_MAIN_COLLAPSE_THREAD,        "\033v" },
@@ -95,6 +96,7 @@ struct binding_t OpMain[] = {
   { "next-undeleted",          OP_MAIN_NEXT_UNDELETED,         "j" },
   { "previous-undeleted",      OP_MAIN_PREV_UNDELETED,         "k" },
   { "limit",                   OP_MAIN_LIMIT,                  "l" },
+  { "link-threads",            OP_MAIN_LINK_THREADS,           "&" },
   { "list-reply",              OP_LIST_REPLY,                  "L" },
   { "mail",                    OP_MAIL,                        "m" },
   { "toggle-new",              OP_TOGGLE_NEW,                  "N" },
@@ -153,6 +155,7 @@ struct binding_t OpMain[] = {
 };
 
 struct binding_t OpPager[] = {
+  { "break-thread",    OP_MAIN_BREAK_THREAD,           "#" },
   { "create-alias",    OP_CREATE_ALIAS,                "a" },
   { "bounce-message",  OP_BOUNCE_MESSAGE,              "b" },
   { "change-folder",   OP_MAIN_CHANGE_FOLDER,          "c" },
@@ -175,6 +178,7 @@ struct binding_t OpPager[] = {
   { "next-entry",      OP_NEXT_ENTRY,                  "J" },
   { "previous-undeleted",OP_MAIN_PREV_UNDELETED,       "k" },
   { "previous-entry",  OP_PREV_ENTRY,                  "K" },
+  { "link-threads",    OP_MAIN_LINK_THREADS,           "&" },
   { "list-reply",      OP_LIST_REPLY,                  "L" },
   { "redraw-screen",   OP_REDRAW,                      "\014" },
   { "mail",            OP_MAIL,                        "m" },
index 59560c214b81f070346f2222a392d4cb2f837c34..7bcd8f0922a4eec39e6d3fdf5aa67a84a06dc15d 100644 (file)
@@ -1065,9 +1065,11 @@ int imap_sync_mailbox (CONTEXT* ctx, int expunge, int* index_hint)
       mutt_message (_("Saving message status flags... [%d/%d]"), n+1,
         ctx->msgcount);
 
-      /* if attachments have been deleted we delete the message and reupload
-       * it. This works better if we're expunging, of course. */
-      if (ctx->hdrs[n]->attach_del)
+      /* if the message has been rethreaded or attachments have been deleted
+       * we delete the message and reupload it.
+       * This works better if we're expunging, of course. */
+      if (ctx->hdrs[n]->refs_changed || ctx->hdrs[n]->irt_changed ||
+         ctx->hdrs[n]->attach_del)
       {
        dprint (3, (debugfile, "imap_sync_mailbox: Attachments to be deleted, falling back to _mutt_save_message\n"));
        if (!appendctx)
diff --git a/mh.c b/mh.c
index eff55bd48df7cb60187baa904f8d3d2447907e1d..2c1c6343be5d5177924e15068127b0b7289930a1 100644 (file)
--- a/mh.c
+++ b/mh.c
@@ -1382,7 +1382,7 @@ static int mh_sync_message (CONTEXT * ctx, int msgno)
 {
   HEADER *h = ctx->hdrs[msgno];
 
-  if (h->attach_del)
+  if (h->attach_del || h->refs_changed || h->irt_changed)
     if (mh_rewrite_message (ctx, msgno) != 0)
       return -1;
 
@@ -1393,9 +1393,9 @@ static int maildir_sync_message (CONTEXT * ctx, int msgno)
 {
   HEADER *h = ctx->hdrs[msgno];
 
-  if (h->attach_del)
+  if (h->attach_del || h->refs_changed || h->irt_changed)
   {
-    /* when doing attachment deletion, fall back to the MH case. */
+    /* when doing attachment deletion/rethreading, fall back to the MH case. */
     if (mh_rewrite_message (ctx, msgno) != 0)
       return (-1);
   }
diff --git a/mutt.h b/mutt.h
index 86e3162352637303ffbf3babda2b59101b58a286..03ec618efa9895ab2cfa66351c2ca245c9a2065a 100644 (file)
--- a/mutt.h
+++ b/mutt.h
@@ -94,6 +94,8 @@
 #define CH_WEED_DELIVERED (1<<13) /* weed eventual Delivered-To headers */
 #define CH_FORCE_FROM  (1<<14) /* give CH_FROM precedence over CH_WEED? */
 #define CH_NOQFROM     (1<<15) /* give CH_FROM precedence over CH_WEED? */
+#define CH_UPDATE_IRT  (1<<16) /* update In-Reply-To: */
+#define CH_UPDATE_REFS (1<<17) /* update References: */
 
 /* flags for mutt_enter_string() */
 #define  M_ALIAS   1      /* do alias "completion" by calling up the alias-menu */
@@ -551,6 +553,7 @@ typedef struct spam_list_t
 void mutt_free_list (LIST **);
 void mutt_free_rx_list (RX_LIST **);
 void mutt_free_spam_list (SPAM_LIST **);
+LIST *mutt_copy_list (LIST *);
 int mutt_matches_ignore (const char *, LIST *);
 
 /* add an element to a list */
@@ -706,6 +709,8 @@ typedef struct header
   unsigned int subject_changed : 1;    /* used for threading */
   unsigned int threaded : 1;           /* used for threading */
   unsigned int display_subject : 1;    /* used for threading */
+  unsigned int irt_changed : 1; /* In-Reply-To changed to link/break threads */
+  unsigned int refs_changed : 1; /* References changed to break thread */
   unsigned int recip_valid : 1;        /* is_recipient is valid */
   unsigned int active : 1;             /* message is not to be removed */
   unsigned int trash : 1;              /* message is marked as trashed on disk.
@@ -746,6 +751,10 @@ typedef struct header
   char *tree;                  /* character string to print thread tree */
   struct thread *thread;
 
+#ifdef USE_IMAP
+  ENVELOPE *new_env;   /* envelope information for rethreading */
+#endif
+
 #ifdef MIXMASTER
   LIST *chain;
 #endif
@@ -810,6 +819,7 @@ typedef struct
   char *pattern;                /* limit pattern string */
   pattern_t *limit_pattern;     /* compiled limit pattern */
   HEADER **hdrs;
+  HEADER *last_tag;            /* last tagged msg. used to link threads */
   THREAD *tree;                        /* top of thread tree */
   HASH *id_hash;               /* hash table by msg id */
   HASH *subj_hash;             /* hash table by subject */
diff --git a/mx.c b/mx.c
index d27a775cbe1048102321e52e65f1f7166501202d..0c1b2ac50d15af694c2aac0aa074c8985759451c 100644 (file)
--- a/mx.c
+++ b/mx.c
@@ -1165,6 +1165,8 @@ int mx_sync_mailbox (CONTEXT *ctx, int *index_hint)
         ctx->deleted = 0;
       }
     }
+    else if (ctx->last_tag && ctx->last_tag->deleted)
+      ctx->last_tag = NULL; /* reset last tagged msg now useless */
   }
 
   /* really only for IMAP - imap_sync_mailbox results in a call to
diff --git a/pager.c b/pager.c
index b558431ae0f4e8cec32608d0d7d8d1f8a717d7d9..6796166f3a8d04dca2d3966cb2ebdc81afc7649b 100644 (file)
--- a/pager.c
+++ b/pager.c
@@ -2498,6 +2498,11 @@ CHECK_IMAP_ACL(IMAP_ACL_WRITE);
       case OP_TAG:
        CHECK_MODE(IsHeader (extra));
        mutt_set_flag (Context, extra->hdr, M_TAG, !extra->hdr->tagged);
+
+       Context->last_tag = extra->hdr->tagged ? extra->hdr :
+         ((Context->last_tag == extra->hdr && !extra->hdr->tagged)
+          ? NULL : Context->last_tag);
+
        redraw = REDRAW_STATUS | REDRAW_INDEX;
        if (option (OPTRESOLVE))
        {
index 248002b8ee018bad11e4cbc421067a3b406ef909..5dcb72737ecff2fe7bf7c485f7769e81703ddbca 100644 (file)
--- a/protos.h
+++ b/protos.h
@@ -165,6 +165,7 @@ void mutt_block_signals (void);
 void mutt_block_signals_system (void);
 void mutt_body_handler (BODY *, STATE *);
 int  mutt_bounce_message (FILE *fp, HEADER *, ADDRESS *);
+void mutt_break_thread (HEADER *);
 void mutt_buffy (char *, size_t);
 int  mutt_buffy_list (void);
 void mutt_canonical_charset (char *, size_t, const char *);
@@ -307,6 +308,7 @@ int mutt_is_list_recipient (int, ADDRESS *, ADDRESS *);
 int mutt_is_subscribed_list (ADDRESS *);
 int mutt_is_text_part (BODY *);
 int mutt_is_valid_mailbox (const char *);
+int mutt_link_threads (HEADER *, HEADER *, CONTEXT *);
 int mutt_lookup_mime_type (BODY *, const char *);
 int mutt_match_rx_list (const char *, RX_LIST *);
 int mutt_match_spam_list (const char *, SPAM_LIST *, char *, int);
index 286266515f04a41ca137f34c76397e2af134e3a5..4c8fe1017d4cf56a10490693f9f4dd4ae86fc7b9 100644 (file)
--- a/thread.c
+++ b/thread.c
@@ -1344,3 +1344,105 @@ HASH *mutt_make_subj_hash (CONTEXT *ctx)
 
   return hash;
 }
+
+static void clean_references (THREAD *brk, THREAD *cur)
+{
+  THREAD *p;
+  LIST *ref = NULL;
+  int done = 0;
+
+  for (; cur; cur = cur->next, done = 0)
+  {
+    /* parse subthread recursively */
+    clean_references (brk, cur->child);
+
+    if (!cur->message)
+      break; /* skip pseudo-message */
+
+    /* Looking for the first bad reference according to the new threading.
+     * Optimal since Mutt stores the references in reverse order, and the
+     * first loop should match immediatly for mails respecting RFC2822. */
+    for (p = brk; !done && p; p = p->parent)
+      for (ref = cur->message->env->references; p->message && ref; ref = ref->next)
+       if (!mutt_strcasecmp (ref->data, p->message->env->message_id))
+       {
+         done = 1;
+         break;
+       }
+
+    if (done)
+    {
+      HEADER *h = cur->message;
+
+      /* clearing the References: header from obsolete Message-Id(s) */
+      mutt_free_list (&ref->next);
+
+#ifdef USE_IMAP
+      if (h->new_env)
+       mutt_free_list (&h->new_env->references);
+      else
+       h->new_env = mutt_new_envelope ();
+
+      h->new_env->references = mutt_copy_list (h->env->references);
+#endif
+
+      h->refs_changed = h->changed = 1;
+    }
+  }
+}
+
+void mutt_break_thread (HEADER *hdr)
+{
+  mutt_free_list (&hdr->env->in_reply_to);
+  mutt_free_list (&hdr->env->references);
+  hdr->irt_changed = hdr->refs_changed = hdr->changed = 1;
+
+#ifdef USE_IMAP
+  if (hdr->new_env)
+  {
+    mutt_free_list (&hdr->new_env->in_reply_to);
+    mutt_free_list (&hdr->new_env->references);
+  }
+  else
+    hdr->new_env = mutt_new_envelope ();
+#endif
+
+  clean_references (hdr->thread, hdr->thread->child);
+}
+
+static int link_threads (HEADER *parent, HEADER *child, CONTEXT *ctx)
+{
+  if (child == parent)
+    return 0;
+
+  mutt_break_thread (child);
+
+  child->env->in_reply_to = mutt_new_list ();
+  child->env->in_reply_to->data = safe_strdup (parent->env->message_id);
+
+#ifdef USE_IMAP
+  child->new_env->in_reply_to = mutt_new_list ();
+  child->new_env->in_reply_to->data = safe_strdup (parent->env->message_id);
+#endif
+  
+  mutt_set_flag (ctx, child, M_TAG, 0);
+  
+  child->irt_changed = child->changed = 1;
+  return 1;
+}
+
+int mutt_link_threads (HEADER *cur, HEADER *last, CONTEXT *ctx)
+{
+  int i, changed = 0;
+
+  if (!last)
+  {
+    for (i = 0; i < ctx->vcount; i++)
+      if (ctx->hdrs[Context->v2r[i]]->tagged)
+       changed |= link_threads (cur, ctx->hdrs[Context->v2r[i]], ctx);
+  }
+  else
+    changed = link_threads (cur, last, ctx);
+
+  return changed;
+}