1 /* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- */
3 * Copyright (C) 2000 Ximian Inc.
5 * Authors: Michael Zucchi <notzed@ximian.com>
7 * This program is free software; you can redistribute it and/or
8 * modify it under the terms of version 2 of the GNU Lesser General Public
9 * License as published by the Free Software Foundation.
11 * This program is distributed in the hope that it will be useful,
12 * but WITHOUT ANY WARRANTY; without even the implied warranty of
13 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
14 * General Public License for more details.
16 * You should have received a copy of the GNU Lesser General Public
17 * License along with this program; if not, write to the
18 * Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
19 * Boston, MA 02110-1301, USA.
22 /* TODO: This could probably be made a camel object, but it isn't really required */
34 #include <sys/types.h>
38 #include <libedataserver/e-memory.h>
40 #include "camel-folder-thread.h"
53 container_add_child(CamelFolderThreadNode *node, CamelFolderThreadNode *child)
55 d(printf("\nAdding child %p to parent %p \n", child, node));
56 child->next = node->child;
62 container_parent_child(CamelFolderThreadNode *parent, CamelFolderThreadNode *child)
64 CamelFolderThreadNode *c, *node;
66 /* are we already the right parent? */
67 if (child->parent == parent)
70 /* would this create a loop? */
71 node = parent->parent;
78 /* are we unparented? */
79 if (child->parent == NULL) {
80 container_add_child(parent, child);
84 /* else remove child from its existing parent, and reparent */
86 c = (CamelFolderThreadNode *)&node->child;
87 d(printf("scanning children:\n"));
89 d(printf(" %p\n", c));
91 d(printf("found node %p\n", child));
92 c->next = c->next->next;
94 container_add_child(parent, child);
100 printf("DAMN, we shouldn't be here!\n");
104 prune_empty(CamelFolderThread *thread, CamelFolderThreadNode **cp)
106 CamelFolderThreadNode *child, *next, *c, *lastc;
108 /* yes, this is intentional */
109 lastc = (CamelFolderThreadNode *)cp;
110 while (lastc->next) {
112 prune_empty(thread, &c->child);
114 d(printf("checking message %p %p (%08x%08x)\n", c,
115 c->message, c->message?c->message->message_id.id.part.hi:0,
116 c->message?c->message->message_id.id.part.lo:0));
117 if (c->message == NULL) {
118 if (c->child == NULL) {
119 d(printf("removing empty node\n"));
120 lastc->next = c->next;
121 m(memset(c, 0xfe, sizeof(*c)));
122 e_memchunk_free(thread->node_chunks, c);
125 if (c->parent || c->child->next==0) {
126 d(printf("promoting child\n"));
127 lastc->next = c->next; /* remove us */
132 child->parent = c->parent;
133 child->next = lastc->next;
146 hashloop(void *key, void *value, void *data)
148 CamelFolderThreadNode *c = value;
149 CamelFolderThreadNode *tail = data;
151 if (c->parent == NULL) {
152 c->next = tail->next;
158 get_root_subject(CamelFolderThreadNode *c)
161 CamelFolderThreadNode *scan;
166 s = (char *)camel_message_info_subject(c->message);
168 /* one of the children will always have a message */
172 s = (char *)camel_message_info_subject(scan->message);
184 if ((s[0] == 'r' || s[0]=='R')
185 && (s[1] == 'e' || s[1]=='E')) {
187 while (isdigit(*p) || (ispunct(*p) && (*p != ':')))
203 /* this can be pretty slow, but not used often */
204 /* clast cannot be null */
206 remove_node(CamelFolderThreadNode **list, CamelFolderThreadNode *node, CamelFolderThreadNode **clast)
208 CamelFolderThreadNode *c;
210 /* this is intentional, even if it looks funny */
211 /* if we have a parent, then we should remove it from the parent list,
212 otherwise we remove it from the root list */
214 c = (CamelFolderThreadNode *)&node->parent->child;
216 c = (CamelFolderThreadNode *)list;
219 if (c->next == node) {
220 if (*clast == c->next)
222 c->next = c->next->next;
228 printf("ERROR: removing node %p failed\n", node);
232 group_root_set(CamelFolderThread *thread, CamelFolderThreadNode **cp)
234 GHashTable *subject_table = g_hash_table_new(g_str_hash, g_str_equal);
235 CamelFolderThreadNode *c, *clast, *scan, *container;
237 /* gather subject lines */
238 d(printf("gathering subject lines\n"));
239 clast = (CamelFolderThreadNode *)cp;
242 c->root_subject = get_root_subject(c);
243 if (c->root_subject) {
244 container = g_hash_table_lookup(subject_table, c->root_subject);
245 if (container == NULL
246 || (container->message == NULL && c->message)
247 || (container->re == TRUE && !c->re)) {
248 g_hash_table_insert(subject_table, c->root_subject, c);
254 /* merge common subjects? */
255 clast = (CamelFolderThreadNode *)cp;
256 while (clast->next) {
258 d(printf("checking %p %s\n", c, c->root_subject));
260 && (container = g_hash_table_lookup(subject_table, c->root_subject))
261 && (container != c)) {
262 d(printf(" matching %p %s\n", container, container->root_subject));
263 if (c->message == NULL && container->message == NULL) {
264 d(printf("merge containers children\n"));
265 /* steal the children from c onto container, and unlink c */
266 scan = (CamelFolderThreadNode *)&container->child;
269 scan->next = c->child;
270 clast->next = c->next;
271 m(memset(c, 0xee, sizeof(*c)));
272 e_memchunk_free(thread->node_chunks, c);
274 } if (c->message == NULL && container->message != NULL) {
275 d(printf("container is non-empty parent\n"));
276 remove_node(cp, container, &clast);
277 container_add_child(c, container);
278 } else if (c->message != NULL && container->message == NULL) {
279 d(printf("container is empty child\n"));
280 clast->next = c->next;
281 container_add_child(container, c);
283 } else if (c->re && !container->re) {
284 d(printf("container is re\n"));
285 clast->next = c->next;
286 container_add_child(container, c);
288 } else if (!c->re && container->re) {
289 d(printf("container is not re\n"));
290 remove_node(cp, container, &clast);
291 container_add_child(c, container);
292 } else if (c->re && container->re) {
293 d(printf("subjects are common %p and %p\n", c, container));
295 /* build a phantom node */
296 remove_node(cp, container, &clast);
297 remove_node(cp, c, &clast);
299 scan = e_memchunk_alloc0(thread->node_chunks);
301 scan->root_subject = c->root_subject;
302 scan->re = c->re && container->re;
303 scan->next = c->next;
305 container_add_child(scan, c);
306 container_add_child(scan, container);
308 g_hash_table_insert(subject_table, scan->root_subject, scan);
314 g_hash_table_destroy(subject_table);
322 dump_tree_rec(struct _tree_info *info, CamelFolderThreadNode *c, int depth)
327 p = alloca(depth*2+1);
328 memset(p, ' ', depth*2);
332 if (g_hash_table_lookup(info->visited, c)) {
333 printf("WARNING: NODE REVISITED: %p\n", c);
335 g_hash_table_insert(info->visited, c, c);
338 printf("%s %p Subject: %s <%08x%08x>\n", p, c, camel_message_info_subject(c->message),
339 camel_message_info_message_id(c->message)->id.part.hi, camel_message_info_message_id(c->message)->id.part.lo);
342 printf("%s %p <empty>\n", p, c);
345 count += dump_tree_rec(info, c->child, depth+1);
352 camel_folder_threaded_messages_dump(CamelFolderThreadNode *c)
355 struct _tree_info info;
357 info.visited = g_hash_table_new(g_direct_hash, g_direct_equal);
358 count = dump_tree_rec(&info, c, 0);
359 g_hash_table_destroy(info.visited);
364 sort_node(const void *a, const void *b)
366 const CamelFolderThreadNode *a1 = ((CamelFolderThreadNode **)a)[0];
367 const CamelFolderThreadNode *b1 = ((CamelFolderThreadNode **)b)[0];
369 /* if we have no message, it must be a dummy node, which
370 also means it must have a child, just use that as the
371 sort data (close enough?) */
372 if (a1->message == NULL)
374 if (b1->message == NULL)
376 if (a1->order == b1->order)
378 if (a1->order < b1->order)
385 sort_thread(CamelFolderThreadNode **cp)
387 CamelFolderThreadNode *c, *head, **carray;
392 /* sort the children while we're at it */
394 sort_thread(&c->child);
400 carray = alloca(size*sizeof(CamelFolderThreadNode *));
408 qsort(carray, size, sizeof(CamelFolderThreadNode *), sort_node);
422 static guint id_hash(void *key)
424 CamelSummaryMessageID *id = (CamelSummaryMessageID *)key;
426 return id->id.part.lo;
429 static gint id_equal(void *a, void *b)
431 return ((CamelSummaryMessageID *)a)->id.id == ((CamelSummaryMessageID *)b)->id.id;
434 /* perform actual threading */
436 thread_summary(CamelFolderThread *thread, GPtrArray *summary)
438 GHashTable *id_table, *no_id_table;
440 CamelFolderThreadNode *c, *child, *head;
442 struct timeval start, end;
445 gettimeofday(&start, NULL);
448 id_table = g_hash_table_new((GHashFunc)id_hash, (GCompareFunc)id_equal);
449 no_id_table = g_hash_table_new(NULL, NULL);
450 for (i=0;i<summary->len;i++) {
451 CamelMessageInfo *mi = summary->pdata[i];
452 const CamelSummaryMessageID *mid = camel_message_info_message_id(mi);
453 const CamelSummaryReferences *references = camel_message_info_references(mi);
456 c = g_hash_table_lookup(id_table, mid);
457 /* check for duplicate messages */
459 /* if duplicate, just make out it is a no-id message, but try and insert it
460 into the right spot in the tree */
461 d(printf("doing: (duplicate message id)\n"));
462 c = e_memchunk_alloc0(thread->node_chunks);
463 g_hash_table_insert(no_id_table, (void *)mi, c);
465 d(printf("doing : %08x%08x (%s)\n", mid->id.part.hi, mid->id.part.lo, camel_message_info_subject(mi)));
466 c = e_memchunk_alloc0(thread->node_chunks);
467 g_hash_table_insert(id_table, (void *)mid, c);
470 d(printf("doing : (no message id)\n"));
471 c = e_memchunk_alloc0(thread->node_chunks);
472 g_hash_table_insert(no_id_table, (void *)mi, c);
481 d(printf("%s (%s) references:\n", G_STRLOC, G_STRFUNC); )
482 for (j=0;j<references->size;j++) {
483 /* should never be empty, but just incase */
484 if (references->references[j].id.id == 0)
487 c = g_hash_table_lookup(id_table, &references->references[j]);
489 d(printf("%s (%s) not found\n", G_STRLOC, G_STRFUNC));
490 c = e_memchunk_alloc0(thread->node_chunks);
491 g_hash_table_insert(id_table, (void *)&references->references[j], c);
494 container_parent_child(c, child);
501 /* build a list of root messages (no parent) */
503 g_hash_table_foreach(id_table, hashloop, &head);
504 g_hash_table_foreach(no_id_table, hashloop, &head);
506 g_hash_table_destroy(id_table);
507 g_hash_table_destroy(no_id_table);
509 /* remove empty parent nodes */
510 prune_empty(thread, &head);
512 /* find any siblings which missed out - but only if we are allowing threading by subject */
514 group_root_set (thread, &head);
517 printf("finished\n");
518 i = camel_folder_threaded_messages_dump(head);
519 printf("%d count, %d items in tree\n", summary->len, i);
524 /* remove any phantom nodes, this could possibly be put in group_root_set()? */
525 c = (CamelFolderThreadNode *)&head;
526 while (c && c->next) {
527 CamelFolderThreadNode *scan, *newtop;
530 if (child->message == NULL) {
531 newtop = child->child;
532 newtop->parent = NULL;
533 /* unlink pseudo node */
536 /* link its siblings onto the end of its children, fix all parent pointers */
537 scan = (CamelFolderThreadNode *)&newtop->child;
541 scan->next = newtop->next;
544 scan->parent = newtop;
547 /* and link the now 'real' node into the list */
548 newtop->next = child->next;
550 m(memset(child, 0xde, sizeof(*child)));
551 e_memchunk_free(thread->node_chunks, child);
557 /* this is only debug assertion stuff */
558 c = (CamelFolderThreadNode *)&head;
561 if (c->message == NULL)
562 g_warning("threading missed removing a pseudo node: %s\n", c->root_subject);
563 if (c->parent != NULL)
564 g_warning("base node has a non-null parent: %s\n", c->root_subject);
570 gettimeofday(&end, NULL);
571 diff = end.tv_sec * 1000 + end.tv_usec/1000;
572 diff -= start.tv_sec * 1000 + start.tv_usec/1000;
573 printf("Message threading %d messages took %ld.%03ld seconds\n",
574 summary->len, diff / 1000, diff % 1000);
579 * camel_folder_thread_messages_new:
581 * @uids: The subset of uid's to thread. If NULL. then thread all
583 * @thread_subject: thread based on subject also
585 * Thread a (subset) of the messages in a folder. And sort the result
588 * If @thread_subject is %TRUE, messages with
589 * related subjects will also be threaded. The default behaviour is to
590 * only thread based on message-id.
592 * This function is probably to be removed soon.
594 * Return value: A CamelFolderThread contianing a tree of CamelFolderThreadNode's
595 * which represent the threaded structure of the messages.
598 camel_folder_thread_messages_new (CamelFolder *folder, GPtrArray *uids, gboolean thread_subject)
600 CamelFolderThread *thread;
601 GHashTable *wanted = NULL;
606 thread = g_malloc(sizeof(*thread));
607 thread->refcount = 1;
608 thread->subject = thread_subject;
610 thread->node_chunks = e_memchunk_new(32, sizeof(CamelFolderThreadNode));
611 thread->folder = folder;
612 camel_object_ref((CamelObject *)folder);
614 /* get all of the summary items of interest in summary order */
616 wanted = g_hash_table_new(g_str_hash, g_str_equal);
617 for (i=0;i<uids->len;i++)
618 g_hash_table_insert(wanted, uids->pdata[i], uids->pdata[i]);
621 fsummary = camel_folder_get_summary(folder);
622 thread->summary = summary = g_ptr_array_new();
624 for (i=0;i<fsummary->len;i++) {
625 CamelMessageInfo *info = fsummary->pdata[i];
627 if (wanted == NULL || g_hash_table_lookup(wanted, camel_message_info_uid(info)) != NULL) {
628 camel_folder_ref_message_info(folder, info);
629 g_ptr_array_add(summary, info);
633 camel_folder_free_summary(folder, fsummary);
635 thread_summary(thread, summary);
638 g_hash_table_destroy(wanted);
643 /* add any still there, in the existing order */
645 add_present_rec(CamelFolderThread *thread, GHashTable *have, GPtrArray *summary, CamelFolderThreadNode *node)
648 const char *uid = camel_message_info_uid(node->message);
650 if (g_hash_table_lookup(have, (char *)uid)) {
651 g_hash_table_remove(have, (char *)camel_message_info_uid(node->message));
652 g_ptr_array_add(summary, (void *) node->message);
654 camel_folder_free_message_info(thread->folder, (CamelMessageInfo *)node->message);
658 add_present_rec(thread, have, summary, node->child);
664 camel_folder_thread_messages_apply(CamelFolderThread *thread, GPtrArray *uids)
669 CamelMessageInfo *info;
671 all = g_ptr_array_new();
672 table = g_hash_table_new(g_str_hash, g_str_equal);
673 for (i=0;i<uids->len;i++)
674 g_hash_table_insert(table, uids->pdata[i], uids->pdata[i]);
676 add_present_rec(thread, table, all, thread->tree);
678 /* add any new ones, in supplied order */
679 for (i=0;i<uids->len;i++)
680 if (g_hash_table_lookup(table, uids->pdata[i]) && (info = camel_folder_get_message_info(thread->folder, uids->pdata[i])))
681 g_ptr_array_add(all, info);
683 g_hash_table_destroy(table);
686 e_memchunk_destroy(thread->node_chunks);
687 thread->node_chunks = e_memchunk_new(32, sizeof(CamelFolderThreadNode));
688 thread_summary(thread, all);
690 g_ptr_array_free(thread->summary, TRUE);
691 thread->summary = all;
695 camel_folder_thread_messages_ref(CamelFolderThread *thread)
701 * camel_folder_thread_messages_unref:
704 * Free all memory associated with the thread descriptor @thread.
707 camel_folder_thread_messages_unref(CamelFolderThread *thread)
709 if (thread->refcount > 1) {
714 if (thread->folder) {
717 for (i=0;i<thread->summary->len;i++)
718 camel_folder_free_message_info(thread->folder, thread->summary->pdata[i]);
719 g_ptr_array_free(thread->summary, TRUE);
720 camel_object_unref((CamelObject *)thread->folder);
722 e_memchunk_destroy(thread->node_chunks);
728 * camel_folder_thread_messages_new_summary:
729 * @summary: Array of CamelMessageInfo's to thread.
731 * Thread a list of MessageInfo's. The summary must remain valid for the
732 * life of the CamelFolderThread created by this function, and it is upto the
733 * caller to ensure this.
735 * Return value: A CamelFolderThread contianing a tree of CamelFolderThreadNode's
736 * which represent the threaded structure of the messages.
739 camel_folder_thread_messages_new_summary(GPtrArray *summary)
741 CamelFolderThread *thread;
744 struct timeval start, end;
747 gettimeofday(&start, NULL);
750 thread = g_malloc(sizeof(*thread));
751 thread->refcount = 1;
753 thread->node_chunks = e_memchunk_new(32, sizeof(CamelFolderThreadNode));
754 thread->folder = NULL;
755 thread->summary = NULL;
757 thread_summary(thread, summary);
762 /* scan the list in depth-first fashion */
764 build_summary_rec(GHashTable *have, GPtrArray *summary, CamelFolderThreadNode *node)
768 g_hash_table_insert(have, (char *)camel_message_info_uid(node->message), node->message);
769 g_ptr_array_add(summary, node);
771 build_summary_rec(have, summary, node->child);
777 camel_folder_thread_messages_add(CamelFolderThread *thread, GPtrArray *summary)
783 /* Instead of working out all the complex in's and out's of
784 trying to do an incremental summary generation, just redo the whole
785 thing with the summary in the current order - so it comes out
788 all = g_ptr_array_new();
789 table = g_hash_table_new(g_str_hash, g_str_equal);
790 build_summary_rec(table, all, thread->tree);
791 for (i=0;i<summary->len;i++) {
792 CamelMessageInfo *info = summary->pdata[i];
794 /* check its not already there, we dont want duplicates */
795 if (g_hash_table_lookup(table, camel_message_info_uid(info)) == NULL)
796 g_ptr_array_add(all, info);
798 g_hash_table_destroy(table);
800 /* reset the tree, and rebuild fully */
802 e_memchunk_empty(thread->node_chunks);
803 thread_summary(thread, all);
807 remove_uid_node_rec(CamelFolderThread *thread, GHashTable *table, CamelFolderThreadNode **list, CamelFolderThreadNode *parent)
809 CamelFolderThreadNode *prev = NULL;
810 CamelFolderThreadNode *node, *next, *child, *rest;
812 node = (CamelFolderThreadNode *)list;
817 remove_uid_node_rec(thread, table, &next->child, next);
819 /* do we have a node to remove? */
820 if (next->message && g_hash_table_lookup(table, (char *)camel_message_info_uid(node->message))) {
839 e_memchunk_free(thread->node_chunks, next);
843 child->parent = parent;
855 node->next = next->next;
856 e_memchunk_free(thread->node_chunks, next);
867 camel_folder_thread_messages_remove(CamelFolderThread *thread, GPtrArray *uids)
872 table = g_hash_table_new(g_str_hash, g_str_equal);
873 for (i=0;i<uids->len;i++)
874 g_hash_table_insert(table, uids->pdata[i], uids->pdata[i]);
876 remove_uid_node_rec(thread, table, &thread->tree, NULL);
877 g_hash_table_destroy(table);