/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- */ /* * Copyright (C) 2000 Ximian Inc. * * Authors: Michael Zucchi * * This program is free software; you can redistribute it and/or * modify it under the terms of version 2 of the GNU General Public * License as published by the Free Software Foundation. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * General Public License for more details. * * You should have received a copy of the GNU General Public * License along with this program; if not, write to the * Free Software Foundation, Inc., 59 Temple Place - Suite 330, * Boston, MA 02111-1307, USA. */ /* TODO: This could probably be made a camel object, but it isn't really required */ #ifdef HAVE_CONFIG_H #include #endif #include #include #include #include #include #include #include #include #include "camel-folder-thread.h" #include "e-util/e-memory.h" #define d(x) /*#define TIMEIT*/ #ifdef TIMEIT #include #include #endif static void container_add_child(CamelFolderThreadNode *node, CamelFolderThreadNode *child) { d(printf("\nAdding child %p to parent %p \n", child, node)); child->next = node->child; node->child = child; child->parent = node; } static void container_parent_child(CamelFolderThreadNode *parent, CamelFolderThreadNode *child) { CamelFolderThreadNode *c, *node; /* are we already the right parent? */ if (child->parent == parent) return; /* would this create a loop? */ node = parent->parent; while (node) { if (node == child) return; node = node->parent; } /* are we unparented? */ if (child->parent == NULL) { container_add_child(parent, child); return; } /* else remove child from its existing parent, and reparent */ node = child->parent; c = (CamelFolderThreadNode *)&node->child; d(printf("scanning children:\n")); while (c->next) { d(printf(" %p\n", c)); if (c->next==child) { d(printf("found node %p\n", child)); c->next = c->next->next; child->parent = NULL; container_add_child(parent, child); return; } c = c->next; } printf("DAMN, we shouldn't be here!\n"); } static void prune_empty(CamelFolderThread *thread, CamelFolderThreadNode **cp) { CamelFolderThreadNode *child, *next, *c, *lastc; /* yes, this is intentional */ lastc = (CamelFolderThreadNode *)cp; while (lastc->next) { c = lastc->next; d(printf("checking message %p %p (%08x%08x)\n", c, c->message, c->message?c->message->message_id.id.part.hi:0, c->message?c->message->message_id.id.part.lo:0)); if (c->message == NULL) { if (c->child == NULL) { d(printf("removing empty node\n")); lastc->next = c->next; e_memchunk_free(thread->node_chunks, c); continue; } if (c->parent || c->child->next==0) { d(printf("promoting child\n")); lastc->next = c->next; /* remove us */ child = c->child; while (child) { next = child->next; child->parent = c->parent; child->next = lastc->next; lastc->next = child; child = next; } continue; } } prune_empty(thread, &c->child); lastc = c; } } static void hashloop(void *key, void *value, void *data) { CamelFolderThreadNode *c = value; CamelFolderThreadNode *tail = data; if (c->parent == NULL) { c->next = tail->next; tail->next = c; } } static char * get_root_subject(CamelFolderThreadNode *c, int *re) { char *s, *p; CamelFolderThreadNode *scan; s = NULL; *re = FALSE; if (c->message) s = (char *)camel_message_info_subject(c->message); else { /* one of the children will always have a message */ scan = c->child; while (scan) { if (scan->message) { s = (char *)camel_message_info_subject(scan->message); break; } scan = scan->next; } } if (s != NULL) { while (*s) { while (isspace(*s)) s++; if (s[0] == 0) break; if ((s[0] == 'r' || s[0]=='R') && (s[1] == 'e' || s[1]=='E')) { p = s+2; while (isdigit(*p) || (ispunct(*p) && (*p != ':'))) p++; if (*p==':') { *re = TRUE; s = p+1; } else break; } else break; } if (*s) return s; } return NULL; } /* this can be pretty slow, but not used often */ /* clast cannot be null */ static void remove_node(CamelFolderThreadNode **list, CamelFolderThreadNode *node, CamelFolderThreadNode **clast) { CamelFolderThreadNode *c; /* this is intentional, even if it looks funny */ /* if we have a parent, then we should remove it from the parent list, otherwise we remove it from the root list */ if (node->parent) { c = (CamelFolderThreadNode *)&node->parent->child; } else { c = (CamelFolderThreadNode *)list; } while (c->next) { if (c->next == node) { if (*clast == c->next) *clast = c; c->next = c->next->next; return; } c = c->next; } printf("ERROR: removing node %p failed\n", node); } static void group_root_set(CamelFolderThread *thread, CamelFolderThreadNode **cp) { GHashTable *subject_table = g_hash_table_new(g_str_hash, g_str_equal); CamelFolderThreadNode *c, *clast, *scan, *container; /* gather subject lines */ d(printf("gathering subject lines\n")); clast = (CamelFolderThreadNode *)cp; c = clast->next; while (c) { c->root_subject = get_root_subject(c, &c->re); if (c->root_subject) { container = g_hash_table_lookup(subject_table, c->root_subject); if (container == NULL || (container->message == NULL && c->message) || (container->re == TRUE && !c->re)) { g_hash_table_insert(subject_table, c->root_subject, c); } } c = c->next; } /* merge common subjects? */ clast = (CamelFolderThreadNode *)cp; while (clast->next) { c = clast->next; d(printf("checking %p %s\n", c, c->root_subject)); if (c->root_subject && (container = g_hash_table_lookup(subject_table, c->root_subject)) && (container != c)) { d(printf(" matching %p %s\n", container, container->root_subject)); if (c->message == NULL && container->message == NULL) { d(printf("merge containers children\n")); /* steal the children from c onto container, and unlink c */ scan = (CamelFolderThreadNode *)&container->child; while (scan->next) scan = scan->next; scan->next = c->child; clast->next = c->next; e_memchunk_free(thread->node_chunks, c); continue; } if (c->message == NULL && container->message != NULL) { d(printf("container is non-empty parent\n")); remove_node(cp, container, &clast); container_add_child(c, container); } else if (c->message != NULL && container->message == NULL) { d(printf("container is empty child\n")); clast->next = c->next; container_add_child(container, c); continue; } else if (c->re && !container->re) { d(printf("container is re\n")); clast->next = c->next; container_add_child(container, c); continue; } else if (!c->re && container->re) { d(printf("container is not re\n")); remove_node(cp, container, &clast); container_add_child(c, container); } else if (c->re && container->re) { d(printf("subjects are common %p and %p\n", c, container)); /* build a phantom node */ remove_node(cp, container, &clast); remove_node(cp, c, &clast); scan = e_memchunk_alloc0(thread->node_chunks); scan->root_subject = c->root_subject; scan->re = c->re && container->re; scan->next = c->next; clast->next = scan; container_add_child(scan, c); container_add_child(scan, container); clast = scan; g_hash_table_insert(subject_table, scan->root_subject, scan); continue; } } clast = c; } g_hash_table_destroy(subject_table); } struct _tree_info { GHashTable *visited; }; static int dump_tree_rec(struct _tree_info *info, CamelFolderThreadNode *c, int depth) { char *p; int count=0; p = alloca(depth*2+1); memset(p, ' ', depth*2); p[depth*2] = 0; while (c) { if (g_hash_table_lookup(info->visited, c)) { printf("WARNING: NODE REVISITED: %p\n", c); } else { g_hash_table_insert(info->visited, c, c); } if (c->message) { printf("%s %p Subject: %s <%.8s>\n", p, c, camel_message_info_subject(c->message), c->message->message_id.id.hash); count += 1; } else { printf("%s %p \n", p, c); } if (c->child) count += dump_tree_rec(info, c->child, depth+1); c = c->next; } return count; } int camel_folder_threaded_messages_dump(CamelFolderThreadNode *c) { int count; struct _tree_info info; info.visited = g_hash_table_new(g_direct_hash, g_direct_equal); count = dump_tree_rec(&info, c, 0); g_hash_table_destroy(info.visited); return count; } static int sort_node(const void *a, const void *b) { const CamelFolderThreadNode *a1 = ((CamelFolderThreadNode **)a)[0]; const CamelFolderThreadNode *b1 = ((CamelFolderThreadNode **)b)[0]; /* if we have no message, it must be a dummy node, which also means it must have a child, just use that as the sort data (close enough?) */ if (a1->message == NULL) a1 = a1->child; if (b1->message == NULL) b1 = b1->child; if (a1->order == b1->order) return 0; if (a1->order < b1->order) return -1; else return 1; } static void sort_thread(CamelFolderThreadNode **cp) { CamelFolderThreadNode *c, *head, **carray; int size=0; c = *cp; while (c) { /* sort the children while we're at it */ if (c->child) sort_thread(&c->child); size++; c = c->next; } if (size<2) return; carray = alloca(size*sizeof(CamelFolderThreadNode *)); c = *cp; size=0; while (c) { carray[size] = c; c = c->next; size++; } qsort(carray, size, sizeof(CamelFolderThreadNode *), sort_node); size--; head = carray[size]; head->next = NULL; size--; do { c = carray[size]; c->next = head; head = c; size--; } while (size>=0); *cp = head; } static guint id_hash(void *key) { CamelSummaryMessageID *id = (CamelSummaryMessageID *)key; return id->id.part.lo; } static gint id_equal(void *a, void *b) { return ((CamelSummaryMessageID *)a)->id.id == ((CamelSummaryMessageID *)b)->id.id; } /* perform actual threading */ static void thread_summary(CamelFolderThread *thread, GPtrArray *summary) { GHashTable *id_table, *no_id_table; int i; CamelFolderThreadNode *c, *child, *head; #ifdef TIMEIT struct timeval start, end; unsigned long diff; gettimeofday(&start, NULL); #endif id_table = g_hash_table_new((GHashFunc)id_hash, (GCompareFunc)id_equal); no_id_table = g_hash_table_new(NULL, NULL); for (i=0;ilen;i++) { CamelMessageInfo *mi = summary->pdata[i]; if (mi->message_id.id.id) { c = g_hash_table_lookup(id_table, &mi->message_id); /* check for duplicate messages */ if (c && c->order) { /* if duplicate, just make out it is a no-id message, but try and insert it into the right spot in the tree */ d(printf("doing: (duplicate message id)\n")); c = e_memchunk_alloc0(thread->node_chunks); g_hash_table_insert(no_id_table, (void *)mi, c); } else if (!c) { d(printf("doing : %08x%08x (%s)\n", mi->message_id.id.part.hi, mi->message_id.id.part.lo, camel_message_info_subject(mi))); c = e_memchunk_alloc0(thread->node_chunks); g_hash_table_insert(id_table, (void *)&mi->message_id, c); } } else { d(printf("doing : (no message id)\n")); c = e_memchunk_alloc0(thread->node_chunks); g_hash_table_insert(no_id_table, (void *)mi, c); } c->message = mi; c->order = i+1; child = c; if (mi->references) { int j; d(printf("references:\n")); for (j=0;jreferences->size;j++) { /* should never be empty, but just incase */ if (mi->references->references[j].id.id == 0) continue; c = g_hash_table_lookup(id_table, &mi->references->references[j]); if (c == NULL) { d(printf("not found\n")); c = e_memchunk_alloc0(thread->node_chunks); g_hash_table_insert(id_table, &mi->references->references[j], c); } if (c!=child) container_parent_child(c, child); child = c; } } } d(printf("\n\n")); /* build a list of root messages (no parent) */ head = NULL; g_hash_table_foreach(id_table, hashloop, &head); g_hash_table_foreach(no_id_table, hashloop, &head); g_hash_table_destroy(id_table); g_hash_table_destroy(no_id_table); /* remove empty parent nodes */ prune_empty(thread, &head); /* find any siblings which missed out - but only if we are allowing threading by subject */ if (thread->subject) group_root_set (thread, &head); #if 0 printf("finished\n"); i = camel_folder_thread_messages_dump(head); printf("%d count, %d items in tree\n", uids->len, i); #endif sort_thread(&head); /* remove any phantom nodes, this could possibly be put in group_root_set()? */ c = (CamelFolderThreadNode *)&head; while (c && c->next) { CamelFolderThreadNode *scan, *newtop; child = c->next; if (child->message == NULL) { newtop = child->child; /* unlink pseudo node */ c->next = newtop; /* link its siblings onto the end of its children */ scan = (CamelFolderThreadNode *)&newtop->child; while (scan->next) scan = scan->next; scan->next = newtop->next; /* and link the now 'real' node into the list */ newtop->next = child->next; c = newtop; e_memchunk_free(thread->node_chunks, child); } else { c = child; } } /* this is only debug assertion stuff */ c = (CamelFolderThreadNode *)&head; while (c->next) { c = c->next; if (c->message == NULL) g_warning("threading missed removing a pseudo node: %s\n", c->root_subject); } thread->tree = head; #ifdef TIMEIT gettimeofday(&end, NULL); diff = end.tv_sec * 1000 + end.tv_usec/1000; diff -= start.tv_sec * 1000 + start.tv_usec/1000; printf("Message threading %d messages took %ld.%03ld seconds\n", summary->len, diff / 1000, diff % 1000); #endif } /** * camel_folder_thread_messages_new: * @folder: * @uids: The subset of uid's to thread. If NULL. then thread all * uid's in @folder. * @thread_subject: thread based on subject also * * Thread a (subset) of the messages in a folder. And sort the result * in summary order. * * If @thread_subject is %TRUE, messages with * related subjects will also be threaded. The default behaviour is to * only thread based on message-id. * * This function is probably to be removed soon. * * Return value: A CamelFolderThread contianing a tree of CamelFolderThreadNode's * which represent the threaded structure of the messages. **/ CamelFolderThread * camel_folder_thread_messages_new (CamelFolder *folder, GPtrArray *uids, gboolean thread_subject) { CamelFolderThread *thread; GHashTable *wanted = NULL; GPtrArray *summary; GPtrArray *fsummary; int i; thread = g_malloc(sizeof(*thread)); thread->refcount = 1; thread->subject = thread_subject; thread->tree = NULL; thread->node_chunks = e_memchunk_new(32, sizeof(CamelFolderThreadNode)); thread->folder = folder; camel_object_ref((CamelObject *)folder); /* get all of the summary items of interest in summary order */ if (uids) { wanted = g_hash_table_new(g_str_hash, g_str_equal); for (i=0;ilen;i++) g_hash_table_insert(wanted, uids->pdata[i], uids->pdata[i]); } fsummary = camel_folder_get_summary(folder); thread->summary = summary = g_ptr_array_new(); for (i=0;ilen;i++) { CamelMessageInfo *info = fsummary->pdata[i]; if (wanted == NULL || g_hash_table_lookup(wanted, camel_message_info_uid(info)) != NULL) { camel_folder_ref_message_info(folder, info); g_ptr_array_add(summary, info); } } camel_folder_free_summary(folder, fsummary); thread_summary(thread, summary); g_hash_table_destroy(wanted); return thread; } /* add any still there, in the existing order */ static void add_present_rec(CamelFolderThread *thread, GHashTable *have, GPtrArray *summary, CamelFolderThreadNode *node) { while (node) { const char *uid = camel_message_info_uid(node->message); if (g_hash_table_lookup(have, (char *)uid)) { g_hash_table_remove(have, (char *)camel_message_info_uid(node->message)); g_ptr_array_add(summary, (void *) node->message); } else { camel_folder_free_message_info(thread->folder, (CamelMessageInfo *)node->message); } if (node->child) add_present_rec(thread, have, summary, node->child); node = node->next; } } void camel_folder_thread_messages_apply(CamelFolderThread *thread, GPtrArray *uids) { int i; GPtrArray *all; GHashTable *table; CamelMessageInfo *info; all = g_ptr_array_new(); table = g_hash_table_new(g_str_hash, g_str_equal); for (i=0;ilen;i++) g_hash_table_insert(table, uids->pdata[i], uids->pdata[i]); add_present_rec(thread, table, all, thread->tree); /* add any new ones, in supplied order */ for (i=0;ilen;i++) if (g_hash_table_lookup(table, uids->pdata[i]) && (info = camel_folder_get_message_info(thread->folder, uids->pdata[i]))) g_ptr_array_add(all, info); g_hash_table_destroy(table); thread_summary(thread, all); g_ptr_array_free(thread->summary, TRUE); thread->summary = all; } void camel_folder_thread_messages_ref(CamelFolderThread *thread) { thread->refcount++; } /** * camel_folder_thread_messages_unref: * @thread: * * Free all memory associated with the thread descriptor @thread. **/ void camel_folder_thread_messages_unref(CamelFolderThread *thread) { if (thread->refcount > 1) { thread->refcount--; return; } if (thread->folder) { int i; for (i=0;isummary->len;i++) camel_folder_free_message_info(thread->folder, thread->summary->pdata[i]); g_ptr_array_free(thread->summary, TRUE); camel_object_unref((CamelObject *)thread->folder); } e_memchunk_destroy(thread->node_chunks); g_free(thread); } #if 0 /** * camel_folder_thread_messages_new_summary: * @summary: Array of CamelMessageInfo's to thread. * * Thread a list of MessageInfo's. The summary must remain valid for the * life of the CamelFolderThread created by this function, and it is upto the * caller to ensure this. * * Return value: A CamelFolderThread contianing a tree of CamelFolderThreadNode's * which represent the threaded structure of the messages. **/ CamelFolderThread * camel_folder_thread_messages_new_summary(GPtrArray *summary) { CamelFolderThread *thread; #ifdef TIMEIT struct timeval start, end; unsigned long diff; gettimeofday(&start, NULL); #endif thread = g_malloc(sizeof(*thread)); thread->refcount = 1; thread->tree = NULL; thread->node_chunks = e_memchunk_new(32, sizeof(CamelFolderThreadNode)); thread->folder = NULL; thread->summary = NULL; thread_summary(thread, summary); return thread; } /* scan the list in depth-first fashion */ static void build_summary_rec(GHashTable *have, GPtrArray *summary, CamelFolderThreadNode *node) { while (node) { if (node->message) g_hash_table_insert(have, (char *)camel_message_info_uid(node->message), node->message); g_ptr_array_add(summary, node); if (node->child) build_summary_rec(have, summary, node->child); node = node->next; } } void camel_folder_thread_messages_add(CamelFolderThread *thread, GPtrArray *summary) { GPtrArray *all; int i; GHashTable *table; /* Instead of working out all the complex in's and out's of trying to do an incremental summary generation, just redo the whole thing with the summary in the current order - so it comes out in the same order */ all = g_ptr_array_new(); table = g_hash_table_new(g_str_hash, g_str_equal); build_summary_rec(table, all, thread->tree); for (i=0;ilen;i++) { CamelMessageInfo *info = summary->pdata[i]; /* check its not already there, we dont want duplicates */ if (g_hash_table_lookup(table, camel_message_info_uid(info)) == NULL) g_ptr_array_add(all, info); } g_hash_table_destroy(table); /* reset the tree, and rebuild fully */ thread->tree = NULL; e_memchunk_empty(thread->node_chunks); thread_summary(thread, all, NULL); } static void remove_uid_node_rec(CamelFolderThread *thread, GHashTable *table, CamelFolderThreadNode **list, CamelFolderThreadNode *parent) { CamelFolderThreadNode *prev = NULL; CamelFolderThreadNode *node, *next, *child, *rest; node = (CamelFolderThreadNode *)list; next = node->next; while (next) { if (next->child) remove_uid_node_rec(thread, table, &next->child, next); /* do we have a node to remove? */ if (next->message && g_hash_table_lookup(table, (char *)camel_message_info_uid(node->message))) { child = next->child; if (child) { /* node next child lchild rest becomes: node child lchild rest */ rest = next->next; node->next = child; e_memchunk_free(thread->node_chunks, next); next = child; do { lchild = child; child->parent = parent; child = child->next; } while (child); lchild->next = rest; } else { /* node next rest becomes: node rest */ node->next = next->next; e_memchunk_free(thread->node_chunks, next); next = node->next; } } else { node = next; next = node->next; } } } void camel_folder_thread_messages_remove(CamelFolderThread *thread, GPtrArray *uids) { GHashTable *table; int i; table = g_hash_table_new(g_str_hash, g_str_equal); for (i=0;ilen;i++) g_hash_table_insert(table, uids->pdata[i], uids->pdata[i]); remove_uid_node_rec(thread, table, &thread->tree, NULL); g_hash_table_destroy(table); } #endif